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
+374 -334
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -158,8 +158,6 @@
kiroRsKey: state.kiroRsKey, kiroRsKey: state.kiroRsKey,
autoRunSkipFailures: state.autoRunSkipFailures, autoRunSkipFailures: state.autoRunSkipFailures,
autoRunFallbackThreadIntervalMinutes: state.autoRunFallbackThreadIntervalMinutes, autoRunFallbackThreadIntervalMinutes: state.autoRunFallbackThreadIntervalMinutes,
autoRunDelayEnabled: state.autoRunDelayEnabled,
autoRunDelayMinutes: state.autoRunDelayMinutes,
autoStepDelaySeconds: state.autoStepDelaySeconds, autoStepDelaySeconds: state.autoStepDelaySeconds,
stepExecutionRangeByFlow: state.stepExecutionRangeByFlow, stepExecutionRangeByFlow: state.stepExecutionRangeByFlow,
signupMethod: state.signupMethod, signupMethod: state.signupMethod,
@@ -502,7 +500,6 @@
}, { }, {
autoRunSessionId: 0, autoRunSessionId: 0,
autoRunTimerPlan: null, autoRunTimerPlan: null,
scheduledAutoRunPlan: null,
}); });
clearStopRequest(); clearStopRequest();
} }
@@ -1159,7 +1156,6 @@
autoRunSessionId: 0, autoRunSessionId: 0,
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
autoRunTimerPlan: null, autoRunTimerPlan: null,
scheduledAutoRunPlan: null,
...getAutoRunStatusPayload(deps.getStopRequested() || stoppedEarly ? 'stopped' : 'complete', { ...getAutoRunStatusPayload(deps.getStopRequested() || stoppedEarly ? 'stopped' : 'complete', {
currentRun: deps.getStopRequested() || stoppedEarly ? afterRuntime.autoRunCurrentRun : afterRuntime.autoRunTotalRuns, currentRun: deps.getStopRequested() || stoppedEarly ? afterRuntime.autoRunCurrentRun : afterRuntime.autoRunTotalRuns,
totalRuns: 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 = {}) { function getAutoRunStatusPayload(phase, payload = {}) {
return { return {
autoRunning: phase === 'scheduled' autoRunning: phase === 'running'
|| phase === 'running'
|| phase === 'waiting_step' || phase === 'waiting_step'
|| phase === 'waiting_email' || phase === 'waiting_email'
|| phase === 'retrying' || phase === 'retrying'
@@ -220,7 +219,6 @@
autoRunTotalRuns: payload.totalRuns ?? 1, autoRunTotalRuns: payload.totalRuns ?? 1,
autoRunAttemptRun: payload.attemptRun ?? 0, autoRunAttemptRun: payload.attemptRun ?? 0,
autoRunSessionId: Math.max(0, Math.floor(Number(payload.sessionId ?? payload.autoRunSessionId) || 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, autoRunCountdownAt: Number.isFinite(Number(payload.countdownAt)) ? Number(payload.countdownAt) : null,
autoRunCountdownTitle: payload.countdownTitle === undefined ? '' : String(payload.countdownTitle || ''), autoRunCountdownTitle: payload.countdownTitle === undefined ? '' : String(payload.countdownTitle || ''),
autoRunCountdownNote: payload.countdownNote === undefined ? '' : String(payload.countdownNote || ''), autoRunCountdownNote: payload.countdownNote === undefined ? '' : String(payload.countdownNote || ''),
+9 -72
View File
@@ -11,12 +11,12 @@
buildPersistentSettingsPayload, buildPersistentSettingsPayload,
broadcastDataUpdate, broadcastDataUpdate,
applyIpProxySettingsFromState, applyIpProxySettingsFromState,
cancelScheduledAutoRun,
checkIcloudSession, checkIcloudSession,
clearAccountRunHistory, clearAccountRunHistory,
deleteAccountRunHistoryRecords, deleteAccountRunHistoryRecords,
clearAutoRunTimerAlarm, clearAutoRunTimerAlarm,
clearFreeReusablePhoneActivation, clearFreeReusablePhoneActivation,
clearGrokSsoCookies,
clearLuckmailRuntimeState, clearLuckmailRuntimeState,
clearYydsMailRuntimeState, clearYydsMailRuntimeState,
clearStopRequest, clearStopRequest,
@@ -144,7 +144,6 @@
normalizeMail2925Accounts, normalizeMail2925Accounts,
normalizePayPalAccounts, normalizePayPalAccounts,
normalizeRunCount, normalizeRunCount,
AUTO_RUN_TIMER_KIND_SCHEDULED_START,
notifyNodeComplete, notifyNodeComplete,
notifyNodeError, notifyNodeError,
patchMail2925Account, patchMail2925Account,
@@ -157,7 +156,6 @@
handleCloudflareSecurityBlocked, handleCloudflareSecurityBlocked,
resetState, resetState,
resumeAutoRun, resumeAutoRun,
scheduleAutoRun,
selectLuckmailPurchase, selectLuckmailPurchase,
switchIpProxy, switchIpProxy,
changeIpProxyExit, changeIpProxyExit,
@@ -1148,6 +1146,13 @@
return await clearFreeReusablePhoneActivation(); return await clearFreeReusablePhoneActivation();
} }
case 'CLEAR_GROK_SSO_COOKIES': {
if (typeof clearGrokSsoCookies !== 'function') {
throw new Error('Grok SSO 清空能力未接入。');
}
return await clearGrokSsoCookies();
}
case 'SET_FREE_REUSABLE_PHONE': { case 'SET_FREE_REUSABLE_PHONE': {
if (typeof setFreeReusablePhoneActivation !== 'function') { if (typeof setFreeReusablePhoneActivation !== 'function') {
throw new Error('白嫖复用手机号记录能力未接入。'); throw new Error('白嫖复用手机号记录能力未接入。');
@@ -1316,7 +1321,7 @@
throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。'); throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。');
} }
if (getPendingAutoRunTimerPlan(state)) { if (getPendingAutoRunTimerPlan(state)) {
throw new Error('已有自动运行倒计时计划,请先取消或立即开始。'); throw new Error('已有线程间隔等待,请先停止或立即继续。');
} }
const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1); const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1);
const autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures); const autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures);
@@ -1326,68 +1331,6 @@
return { ok: true }; 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': { case 'SKIP_AUTO_RUN_COUNTDOWN': {
clearStopRequest(); clearStopRequest();
if (message.source === 'sidepanel') { if (message.source === 'sidepanel') {
@@ -1480,16 +1423,10 @@
|| (nextPlusModeEnabled && plusPaymentChanged) || (nextPlusModeEnabled && plusPaymentChanged)
|| (nextPlusModeEnabled && plusAccountAccessStrategyChanged) || (nextPlusModeEnabled && plusAccountAccessStrategyChanged)
|| phoneSignupReloginAfterBindEmailChanged; || phoneSignupReloginAfterBindEmailChanged;
const oauthFlowTimeoutDisabled = Object.prototype.hasOwnProperty.call(updates, 'oauthFlowTimeoutEnabled')
&& updates.oauthFlowTimeoutEnabled === false;
const canonicalSettingsUpdates = await setPersistentSettings(updates); const canonicalSettingsUpdates = await setPersistentSettings(updates);
const stateUpdates = { const stateUpdates = {
...canonicalSettingsUpdates, ...canonicalSettingsUpdates,
...sessionUpdates, ...sessionUpdates,
...(oauthFlowTimeoutDisabled ? {
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
} : {}),
}; };
if (Object.prototype.hasOwnProperty.call(canonicalSettingsUpdates, 'activeFlowId') if (Object.prototype.hasOwnProperty.call(canonicalSettingsUpdates, 'activeFlowId')
&& !Object.prototype.hasOwnProperty.call(stateUpdates, 'flowId')) { && !Object.prototype.hasOwnProperty.call(stateUpdates, 'flowId')) {
+2 -1
View File
@@ -33,7 +33,6 @@
contributionAdapterIds: [], contributionAdapterIds: [],
supportedTargetIds: [], supportedTargetIds: [],
supportsLuckmail: false, supportsLuckmail: false,
supportsOauthTimeoutBudget: false,
canSwitchFlow: true, canSwitchFlow: true,
stepDefinitionMode: 'default', stepDefinitionMode: 'default',
targetSelectorLabel: '来源', targetSelectorLabel: '来源',
@@ -59,6 +58,7 @@
const DEFAULT_TARGET_CAPABILITIES = Object.freeze({ const DEFAULT_TARGET_CAPABILITIES = Object.freeze({
supportsPhoneSignup: true, supportsPhoneSignup: true,
requiresPhoneSignupWarning: false, requiresPhoneSignupWarning: false,
usesOauthTimeoutBudget: false,
supportedPlusAccountAccessStrategies: Object.freeze([PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]), supportedPlusAccountAccessStrategies: Object.freeze([PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]),
}); });
@@ -405,6 +405,7 @@
targetId: effectiveTargetId, targetId: effectiveTargetId,
plusAccountAccessStrategy: effectivePlusAccountAccessStrategy, plusAccountAccessStrategy: effectivePlusAccountAccessStrategy,
plusModeEnabled: runtimeLocks.plusModeEnabled, plusModeEnabled: runtimeLocks.plusModeEnabled,
phoneVerificationEnabled: runtimeLocks.phoneVerificationEnabled,
signupMethod: effectiveSignupMethod, signupMethod: effectiveSignupMethod,
}, },
supportedPanelModes: supportedTargetIds, supportedPanelModes: supportedTargetIds,
+14 -1
View File
@@ -18,7 +18,6 @@
contributionAdapterIds: [], contributionAdapterIds: [],
supportedTargetIds: [], supportedTargetIds: [],
supportsLuckmail: false, supportsLuckmail: false,
supportsOauthTimeoutBudget: false,
canSwitchFlow: true, canSwitchFlow: true,
stepDefinitionMode: 'default', stepDefinitionMode: 'default',
targetSelectorLabel: '\u6765\u6e90', targetSelectorLabel: '\u6765\u6e90',
@@ -49,6 +48,20 @@
label: 'IP \u4ee3\u7406', label: 'IP \u4ee3\u7406',
sectionIds: ['ip-proxy-section'], 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() { function buildFlowDefinitions() {
+1 -3
View File
@@ -209,8 +209,7 @@
function getAutoRunStatusPayload(phase, payload = {}) { function getAutoRunStatusPayload(phase, payload = {}) {
return { return {
autoRunning: phase === 'scheduled' autoRunning: phase === 'running'
|| phase === 'running'
|| phase === 'waiting_step' || phase === 'waiting_step'
|| phase === 'waiting_email' || phase === 'waiting_email'
|| phase === 'retrying' || phase === 'retrying'
@@ -220,7 +219,6 @@
autoRunTotalRuns: payload.totalRuns ?? 1, autoRunTotalRuns: payload.totalRuns ?? 1,
autoRunAttemptRun: payload.attemptRun ?? 0, autoRunAttemptRun: payload.attemptRun ?? 0,
autoRunSessionId: Math.max(0, Math.floor(Number(payload.sessionId ?? payload.autoRunSessionId) || 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, autoRunCountdownAt: Number.isFinite(Number(payload.countdownAt)) ? Number(payload.countdownAt) : null,
autoRunCountdownTitle: payload.countdownTitle === undefined ? '' : String(payload.countdownTitle || ''), autoRunCountdownTitle: payload.countdownTitle === undefined ? '' : String(payload.countdownTitle || ''),
autoRunCountdownNote: payload.countdownNote === undefined ? '' : String(payload.countdownNote || ''), autoRunCountdownNote: payload.countdownNote === undefined ? '' : String(payload.countdownNote || ''),
+424 -334
View File
@@ -17,6 +17,23 @@
return value; return value;
} }
function mergePlainObjects(baseValue = {}, patchValue = {}) {
if (Array.isArray(patchValue)) {
return patchValue.map((entry) => cloneValue(entry));
}
if (!isPlainObject(patchValue)) {
return patchValue === undefined ? cloneValue(baseValue) : patchValue;
}
const baseObject = isPlainObject(baseValue) ? baseValue : {};
const next = {
...cloneValue(baseObject),
};
Object.entries(patchValue).forEach(([key, value]) => {
next[key] = mergePlainObjects(baseObject[key], value);
});
return next;
}
function normalizeStepExecutionRangeEntry(value = {}, fallback = {}) { function normalizeStepExecutionRangeEntry(value = {}, fallback = {}) {
const source = isPlainObject(value) ? value : {}; const source = isPlainObject(value) ? value : {};
const fallbackSource = isPlainObject(fallback) ? fallback : {}; const fallbackSource = isPlainObject(fallback) ? fallback : {};
@@ -47,6 +64,19 @@
const normalizeTargetId = typeof flowRegistry.normalizeTargetId === 'function' const normalizeTargetId = typeof flowRegistry.normalizeTargetId === 'function'
? flowRegistry.normalizeTargetId ? flowRegistry.normalizeTargetId
: ((_flowId, value = '', fallback = '') => String(value || fallback || '').trim().toLowerCase()); : ((_flowId, value = '', fallback = '') => String(value || fallback || '').trim().toLowerCase());
const getRegisteredFlowIds = typeof flowRegistry.getRegisteredFlowIds === 'function'
? flowRegistry.getRegisteredFlowIds
: (() => ['openai', 'kiro']);
const getFlowDefinition = typeof flowRegistry.getFlowDefinition === 'function'
? flowRegistry.getFlowDefinition
: (() => null);
const getDefaultTargetId = typeof flowRegistry.getDefaultTargetId === 'function'
? flowRegistry.getDefaultTargetId
: ((flowId) => (flowId === 'kiro' ? defaultKiroTargetId : defaultOpenAiTargetId));
const getTargetDefinitions = typeof flowRegistry.getTargetDefinitions === 'function'
? flowRegistry.getTargetDefinitions
: (() => ({}));
const normalizePlusAccountAccessStrategy = (value = '') => { const normalizePlusAccountAccessStrategy = (value = '') => {
const normalized = String(value || '').trim().toLowerCase(); const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'sub2api_codex_session') { if (normalized === 'sub2api_codex_session') {
@@ -58,6 +88,92 @@
return 'oauth'; return 'oauth';
}; };
function getCanonicalFlowIds() {
const ids = Array.isArray(getRegisteredFlowIds())
? getRegisteredFlowIds()
: [];
const seen = new Set();
return ids
.map((flowId) => normalizeFlowId(flowId, defaultFlowId))
.filter((flowId) => {
if (!flowId || seen.has(flowId)) {
return false;
}
seen.add(flowId);
return true;
});
}
function getFlowSettingsDefaults(flowId) {
const definition = getFlowDefinition(flowId) || {};
return isPlainObject(definition.settingsDefaults) ? definition.settingsDefaults : {};
}
function getDefaultStepExecutionRange(flowId) {
const flowDefaults = getFlowSettingsDefaults(flowId);
const range = flowDefaults?.autoRun?.stepExecutionRange;
if (isPlainObject(range)) {
return normalizeStepExecutionRangeEntry(range, { enabled: false, fromStep: 1, toStep: 1 });
}
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() { function buildDefaultSettingsState() {
return { return {
schemaVersion: 5, schemaVersion: 5,
@@ -75,67 +191,7 @@
mode: 'account', mode: 'account',
}, },
}, },
flows: { flows: buildDefaultFlows(),
openai: {
selectedTargetId: defaultOpenAiTargetId,
targets: {
cpa: {
vpsUrl: '',
vpsPassword: '',
localCpaStep9Mode: 'submit',
},
sub2api: {
sub2apiUrl: '',
sub2apiEmail: '',
sub2apiPassword: '',
sub2apiGroupName: 'codex',
sub2apiGroupNames: ['codex', 'openai-plus'],
sub2apiAccountPriority: 1,
sub2apiDefaultProxyName: '',
},
codex2api: {
codex2apiUrl: '',
codex2apiAdminKey: '',
},
},
signup: {
signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false,
},
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal-hosted',
plusAccountAccessStrategy: 'oauth',
hostedCheckoutVerificationUrl: '',
hostedCheckoutPhoneNumber: '',
plusHostedCheckoutOauthDelaySeconds: 3,
},
autoRun: {
stepExecutionRange: {
enabled: false,
fromStep: 1,
toStep: 11,
},
},
},
kiro: {
selectedTargetId: defaultKiroTargetId,
targets: {
'kiro-rs': {
baseUrl: defaultKiroRsUrl,
apiKey: '',
},
},
autoRun: {
stepExecutionRange: {
enabled: false,
fromStep: 1,
toStep: 9,
},
},
},
},
}; };
} }
@@ -147,6 +203,258 @@
return cloneValue(resolvedValue); return cloneValue(resolvedValue);
} }
function normalizeFlowTargetState(flowId, targetId, nested = {}, defaults = {}) {
const targetState = mergePlainObjects(defaults, nested);
if (flowId === 'openai' && targetId === 'cpa') {
return {
...targetState,
vpsUrl: String(targetState.vpsUrl ?? '').trim(),
vpsPassword: String(targetState.vpsPassword ?? ''),
localCpaStep9Mode: String(targetState.localCpaStep9Mode ?? 'submit').trim() || 'submit',
};
}
if (flowId === 'openai' && targetId === 'sub2api') {
return {
...targetState,
sub2apiUrl: String(targetState.sub2apiUrl ?? '').trim(),
sub2apiEmail: String(targetState.sub2apiEmail ?? '').trim(),
sub2apiPassword: String(targetState.sub2apiPassword ?? ''),
sub2apiGroupName: String(targetState.sub2apiGroupName ?? 'codex').trim() || 'codex',
sub2apiGroupNames: Array.isArray(targetState.sub2apiGroupNames)
? targetState.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: ['codex', 'openai-plus'],
sub2apiAccountPriority: Math.max(1, Number(targetState.sub2apiAccountPriority) || 1),
sub2apiDefaultProxyName: String(targetState.sub2apiDefaultProxyName ?? '').trim(),
};
}
if (flowId === 'openai' && targetId === 'codex2api') {
return {
...targetState,
codex2apiUrl: String(targetState.codex2apiUrl ?? '').trim(),
codex2apiAdminKey: String(targetState.codex2apiAdminKey ?? '').trim(),
};
}
if (flowId === 'kiro' && targetId === 'kiro-rs') {
return {
...targetState,
baseUrl: String(targetState.baseUrl ?? defaultKiroRsUrl).trim() || defaultKiroRsUrl,
apiKey: String(targetState.apiKey ?? ''),
};
}
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 = {}) { function normalizeSettingsState(input = {}, options = {}) {
const defaults = buildDefaultSettingsState(); const defaults = buildDefaultSettingsState();
const nested = isPlainObject(input?.settingsState) const nested = isPlainObject(input?.settingsState)
@@ -159,31 +467,7 @@
?? defaults.activeFlowId, ?? defaults.activeFlowId,
defaults.activeFlowId defaults.activeFlowId
); );
const openaiSelectedTargetId = normalizeTargetId( const normalized = {
'openai',
nested?.flows?.openai?.selectedTargetId
?? nested?.flows?.openai?.integrationTargetId
?? (activeFlowId === 'openai'
? (input?.selectedTargetId ?? input?.targetId)
: undefined)
?? defaults.flows.openai.selectedTargetId,
defaults.flows.openai.selectedTargetId
);
const kiroSelectedTargetId = normalizeTargetId(
'kiro',
nested?.flows?.kiro?.selectedTargetId
?? nested?.flows?.kiro?.targetId
?? (activeFlowId === 'kiro'
? (input?.selectedTargetId ?? input?.targetId)
: undefined)
?? defaults.flows.kiro.selectedTargetId,
defaults.flows.kiro.selectedTargetId
);
const stepExecutionRangeByFlow = isPlainObject(input?.stepExecutionRangeByFlow)
? input.stepExecutionRangeByFlow
: {};
return {
schemaVersion: Number(input?.settingsSchemaVersion || nested?.schemaVersion || defaults.schemaVersion) || defaults.schemaVersion, schemaVersion: Number(input?.settingsSchemaVersion || nested?.schemaVersion || defaults.schemaVersion) || defaults.schemaVersion,
activeFlowId, activeFlowId,
services: { services: {
@@ -219,203 +503,22 @@
).trim(), ).trim(),
}, },
}, },
flows: { flows: {},
openai: {
selectedTargetId: openaiSelectedTargetId,
targets: {
cpa: {
...defaults.flows.openai.targets.cpa,
...getTargetValue(
nested,
(state) => state.flows?.openai?.targets?.cpa,
(state) => state.flows?.openai?.integrationTargets?.cpa
),
vpsUrl: String(
input?.vpsUrl
?? nested?.flows?.openai?.targets?.cpa?.vpsUrl
?? nested?.flows?.openai?.integrationTargets?.cpa?.vpsUrl
?? ''
).trim(),
vpsPassword: String(
input?.vpsPassword
?? nested?.flows?.openai?.targets?.cpa?.vpsPassword
?? nested?.flows?.openai?.integrationTargets?.cpa?.vpsPassword
?? ''
),
localCpaStep9Mode: String(
input?.localCpaStep9Mode
?? nested?.flows?.openai?.targets?.cpa?.localCpaStep9Mode
?? nested?.flows?.openai?.integrationTargets?.cpa?.localCpaStep9Mode
?? defaults.flows.openai.targets.cpa.localCpaStep9Mode
).trim() || defaults.flows.openai.targets.cpa.localCpaStep9Mode,
},
sub2api: {
...defaults.flows.openai.targets.sub2api,
...getTargetValue(
nested,
(state) => state.flows?.openai?.targets?.sub2api,
(state) => state.flows?.openai?.integrationTargets?.sub2api
),
sub2apiUrl: String(
input?.sub2apiUrl
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiUrl
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiUrl
?? ''
).trim(),
sub2apiEmail: String(
input?.sub2apiEmail
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiEmail
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiEmail
?? ''
).trim(),
sub2apiPassword: String(
input?.sub2apiPassword
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiPassword
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiPassword
?? ''
),
sub2apiGroupName: String(
input?.sub2apiGroupName
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiGroupName
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiGroupName
?? defaults.flows.openai.targets.sub2api.sub2apiGroupName
).trim() || defaults.flows.openai.targets.sub2api.sub2apiGroupName,
sub2apiGroupNames: Array.isArray(input?.sub2apiGroupNames)
? input.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: (Array.isArray(nested?.flows?.openai?.targets?.sub2api?.sub2apiGroupNames)
? nested.flows.openai.targets.sub2api.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: (Array.isArray(nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiGroupNames)
? nested.flows.openai.integrationTargets.sub2api.sub2apiGroupNames.map((entry) => String(entry || '').trim()).filter(Boolean)
: [...defaults.flows.openai.targets.sub2api.sub2apiGroupNames])),
sub2apiAccountPriority: Math.max(1, Number(
input?.sub2apiAccountPriority
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiAccountPriority
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiAccountPriority
?? defaults.flows.openai.targets.sub2api.sub2apiAccountPriority
) || defaults.flows.openai.targets.sub2api.sub2apiAccountPriority),
sub2apiDefaultProxyName: String(
input?.sub2apiDefaultProxyName
?? nested?.flows?.openai?.targets?.sub2api?.sub2apiDefaultProxyName
?? nested?.flows?.openai?.integrationTargets?.sub2api?.sub2apiDefaultProxyName
?? ''
).trim(),
},
codex2api: {
...defaults.flows.openai.targets.codex2api,
...getTargetValue(
nested,
(state) => state.flows?.openai?.targets?.codex2api,
(state) => state.flows?.openai?.integrationTargets?.codex2api
),
codex2apiUrl: String(
input?.codex2apiUrl
?? nested?.flows?.openai?.targets?.codex2api?.codex2apiUrl
?? nested?.flows?.openai?.integrationTargets?.codex2api?.codex2apiUrl
?? ''
).trim(),
codex2apiAdminKey: String(
input?.codex2apiAdminKey
?? nested?.flows?.openai?.targets?.codex2api?.codex2apiAdminKey
?? nested?.flows?.openai?.integrationTargets?.codex2api?.codex2apiAdminKey
?? ''
).trim(),
},
},
signup: {
signupMethod: String(
input?.signupMethod
?? nested?.flows?.openai?.signup?.signupMethod
?? defaults.flows.openai.signup.signupMethod
).trim().toLowerCase() === 'phone' ? 'phone' : 'email',
phoneVerificationEnabled: Boolean(
input?.phoneVerificationEnabled
?? nested?.flows?.openai?.signup?.phoneVerificationEnabled
?? defaults.flows.openai.signup.phoneVerificationEnabled
),
phoneSignupReloginAfterBindEmailEnabled: Boolean(
input?.phoneSignupReloginAfterBindEmailEnabled
?? nested?.flows?.openai?.signup?.phoneSignupReloginAfterBindEmailEnabled
?? defaults.flows.openai.signup.phoneSignupReloginAfterBindEmailEnabled
),
},
plus: {
plusModeEnabled: Boolean(
input?.plusModeEnabled
?? nested?.flows?.openai?.plus?.plusModeEnabled
?? defaults.flows.openai.plus.plusModeEnabled
),
plusPaymentMethod: String(
input?.plusPaymentMethod
?? nested?.flows?.openai?.plus?.plusPaymentMethod
?? defaults.flows.openai.plus.plusPaymentMethod
).trim() || defaults.flows.openai.plus.plusPaymentMethod,
plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(
input?.plusAccountAccessStrategy
?? nested?.flows?.openai?.plus?.plusAccountAccessStrategy
?? defaults.flows.openai.plus.plusAccountAccessStrategy
),
hostedCheckoutVerificationUrl: String(
input?.hostedCheckoutVerificationUrl
?? nested?.flows?.openai?.plus?.hostedCheckoutVerificationUrl
?? defaults.flows.openai.plus.hostedCheckoutVerificationUrl
).trim(),
hostedCheckoutPhoneNumber: String(
input?.hostedCheckoutPhoneNumber
?? nested?.flows?.openai?.plus?.hostedCheckoutPhoneNumber
?? defaults.flows.openai.plus.hostedCheckoutPhoneNumber
).trim(),
plusHostedCheckoutOauthDelaySeconds: (() => {
const numeric = Number(
input?.plusHostedCheckoutOauthDelaySeconds
?? nested?.flows?.openai?.plus?.plusHostedCheckoutOauthDelaySeconds
?? defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds
);
return Math.min(120, Math.max(0, Math.floor(Number.isFinite(numeric) ? numeric : defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds)));
})(),
},
autoRun: {
stepExecutionRange: normalizeStepExecutionRangeEntry(
stepExecutionRangeByFlow.openai
?? nested?.flows?.openai?.autoRun?.stepExecutionRange
?? {},
defaults.flows.openai.autoRun.stepExecutionRange
),
},
},
kiro: {
selectedTargetId: kiroSelectedTargetId,
targets: {
'kiro-rs': {
...defaults.flows.kiro.targets['kiro-rs'],
...getTargetValue(
nested,
(state) => state.flows?.kiro?.targets?.['kiro-rs']
),
baseUrl: String(
input?.kiroRsUrl
?? input?.kiroRsBaseUrl
?? nested?.flows?.kiro?.targets?.['kiro-rs']?.baseUrl
?? defaults.flows.kiro.targets['kiro-rs'].baseUrl
).trim() || defaults.flows.kiro.targets['kiro-rs'].baseUrl,
apiKey: String(
input?.kiroRsKey
?? input?.kiroRsApiKey
?? nested?.flows?.kiro?.targets?.['kiro-rs']?.apiKey
?? defaults.flows.kiro.targets['kiro-rs'].apiKey
),
},
},
autoRun: {
stepExecutionRange: normalizeStepExecutionRangeEntry(
stepExecutionRangeByFlow.kiro
?? nested?.flows?.kiro?.autoRun?.stepExecutionRange
?? {},
defaults.flows.kiro.autoRun.stepExecutionRange
),
},
},
},
}; };
getCanonicalFlowIds().forEach((flowId) => {
normalized.flows[flowId] = normalizeFlowSettings(flowId, {
...input,
activeFlowId,
}, nested, defaults);
});
if (normalized.flows.openai) {
normalized.flows.openai = normalizeOpenAiSettings(input, nested, defaults, normalized.flows.openai);
}
if (normalized.flows.kiro) {
normalized.flows.kiro = normalizeKiroSettings(input, defaults, normalized.flows.kiro);
}
return normalized;
} }
function mergeSettingsState(baseValue = {}, patchValue = {}) { function mergeSettingsState(baseValue = {}, patchValue = {}) {
@@ -425,24 +528,8 @@
activeFlowId: patchValue?.activeFlowId ?? baseSettingsState.activeFlowId, activeFlowId: patchValue?.activeFlowId ?? baseSettingsState.activeFlowId,
}); });
function mergeRecursive(baseNode, patchNode) {
if (Array.isArray(patchNode)) {
return patchNode.map((entry) => cloneValue(entry));
}
if (!isPlainObject(patchNode)) {
return patchNode === undefined ? cloneValue(baseNode) : patchNode;
}
const next = {
...cloneValue(isPlainObject(baseNode) ? baseNode : {}),
};
Object.entries(patchNode).forEach(([key, value]) => {
next[key] = mergeRecursive(baseNode?.[key], value);
});
return next;
}
return normalizeSettingsState({ return normalizeSettingsState({
settingsState: mergeRecursive(baseSettingsState, patchSettingsState), settingsState: mergePlainObjects(baseSettingsState, patchSettingsState),
}); });
} }
@@ -459,7 +546,7 @@
return normalizeTargetId( return normalizeTargetId(
normalizedFlowId, normalizedFlowId,
flowSettings?.selectedTargetId, flowSettings?.selectedTargetId,
normalizedFlowId === 'kiro' ? defaultKiroTargetId : defaultOpenAiTargetId getDefaultTargetId(normalizedFlowId)
); );
} }
@@ -476,16 +563,16 @@
function buildStepExecutionRangeByFlow(settingsState = {}) { function buildStepExecutionRangeByFlow(settingsState = {}) {
const normalizedState = normalizeSettingsState(settingsState); const normalizedState = normalizeSettingsState(settingsState);
return { const defaults = buildDefaultSettingsState();
openai: normalizeStepExecutionRangeEntry( return Object.fromEntries(
normalizedState?.flows?.openai?.autoRun?.stepExecutionRange, Object.entries(normalizedState.flows || {}).map(([flowId, flowSettings]) => [
buildDefaultSettingsState().flows.openai.autoRun.stepExecutionRange flowId,
normalizeStepExecutionRangeEntry(
flowSettings?.autoRun?.stepExecutionRange,
defaults.flows?.[flowId]?.autoRun?.stepExecutionRange || getDefaultStepExecutionRange(flowId)
), ),
kiro: normalizeStepExecutionRangeEntry( ])
normalizedState?.flows?.kiro?.autoRun?.stepExecutionRange, );
buildDefaultSettingsState().flows.kiro.autoRun.stepExecutionRange
),
};
} }
function buildSettingsView(settingsState = {}, baseInput = {}) { function buildSettingsView(settingsState = {}, baseInput = {}) {
@@ -493,38 +580,41 @@
const next = { const next = {
...(isPlainObject(baseInput) ? cloneValue(baseInput) : {}), ...(isPlainObject(baseInput) ? cloneValue(baseInput) : {}),
}; };
const openaiState = normalizedState.flows.openai; const openaiState = normalizedState.flows.openai || buildDefaultFlowSettings('openai');
const kiroState = normalizedState.flows.kiro; const kiroState = normalizedState.flows.kiro || buildDefaultFlowSettings('kiro');
const grokState = normalizedState.flows.grok || buildDefaultFlowSettings('grok');
next.activeFlowId = normalizedState.activeFlowId; next.activeFlowId = normalizedState.activeFlowId;
next.targetId = getSelectedTargetId(normalizedState, normalizedState.activeFlowId); next.targetId = getSelectedTargetId(normalizedState, normalizedState.activeFlowId);
next.vpsUrl = openaiState.targets.cpa.vpsUrl; next.vpsUrl = openaiState.targets.cpa?.vpsUrl || '';
next.vpsPassword = openaiState.targets.cpa.vpsPassword; next.vpsPassword = openaiState.targets.cpa?.vpsPassword || '';
next.localCpaStep9Mode = openaiState.targets.cpa.localCpaStep9Mode; next.localCpaStep9Mode = openaiState.targets.cpa?.localCpaStep9Mode || 'submit';
next.sub2apiUrl = openaiState.targets.sub2api.sub2apiUrl; next.sub2apiUrl = openaiState.targets.sub2api?.sub2apiUrl || '';
next.sub2apiEmail = openaiState.targets.sub2api.sub2apiEmail; next.sub2apiEmail = openaiState.targets.sub2api?.sub2apiEmail || '';
next.sub2apiPassword = openaiState.targets.sub2api.sub2apiPassword; next.sub2apiPassword = openaiState.targets.sub2api?.sub2apiPassword || '';
next.sub2apiGroupName = openaiState.targets.sub2api.sub2apiGroupName; next.sub2apiGroupName = openaiState.targets.sub2api?.sub2apiGroupName || 'codex';
next.sub2apiGroupNames = cloneValue(openaiState.targets.sub2api.sub2apiGroupNames); next.sub2apiGroupNames = cloneValue(openaiState.targets.sub2api?.sub2apiGroupNames || ['codex', 'openai-plus']);
next.sub2apiAccountPriority = openaiState.targets.sub2api.sub2apiAccountPriority; next.sub2apiAccountPriority = openaiState.targets.sub2api?.sub2apiAccountPriority || 1;
next.sub2apiDefaultProxyName = openaiState.targets.sub2api.sub2apiDefaultProxyName; next.sub2apiDefaultProxyName = openaiState.targets.sub2api?.sub2apiDefaultProxyName || '';
next.codex2apiUrl = openaiState.targets.codex2api.codex2apiUrl; next.codex2apiUrl = openaiState.targets.codex2api?.codex2apiUrl || '';
next.codex2apiAdminKey = openaiState.targets.codex2api.codex2apiAdminKey; next.codex2apiAdminKey = openaiState.targets.codex2api?.codex2apiAdminKey || '';
next.customPassword = normalizedState.services.account.customPassword; next.customPassword = normalizedState.services.account.customPassword;
next.signupMethod = openaiState.signup.signupMethod; next.signupMethod = openaiState.signup?.signupMethod || 'email';
next.phoneVerificationEnabled = openaiState.signup.phoneVerificationEnabled; next.phoneVerificationEnabled = Boolean(openaiState.signup?.phoneVerificationEnabled);
next.phoneSignupReloginAfterBindEmailEnabled = openaiState.signup.phoneSignupReloginAfterBindEmailEnabled; next.phoneSignupReloginAfterBindEmailEnabled = Boolean(openaiState.signup?.phoneSignupReloginAfterBindEmailEnabled);
next.plusModeEnabled = openaiState.plus.plusModeEnabled; next.plusModeEnabled = Boolean(openaiState.plus?.plusModeEnabled);
next.plusPaymentMethod = openaiState.plus.plusPaymentMethod; next.plusPaymentMethod = openaiState.plus?.plusPaymentMethod || 'paypal-hosted';
next.plusAccountAccessStrategy = openaiState.plus.plusAccountAccessStrategy; next.plusAccountAccessStrategy = openaiState.plus?.plusAccountAccessStrategy || 'oauth';
next.hostedCheckoutVerificationUrl = openaiState.plus.hostedCheckoutVerificationUrl; next.hostedCheckoutVerificationUrl = openaiState.plus?.hostedCheckoutVerificationUrl || '';
next.hostedCheckoutPhoneNumber = openaiState.plus.hostedCheckoutPhoneNumber; next.hostedCheckoutPhoneNumber = openaiState.plus?.hostedCheckoutPhoneNumber || '';
next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus.plusHostedCheckoutOauthDelaySeconds; next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus?.plusHostedCheckoutOauthDelaySeconds ?? 3;
next.mailProvider = normalizedState.services.email.provider; next.mailProvider = normalizedState.services.email.provider;
next.ipProxyEnabled = normalizedState.services.proxy.enabled; next.ipProxyEnabled = normalizedState.services.proxy.enabled;
next.ipProxyService = normalizedState.services.proxy.provider; next.ipProxyService = normalizedState.services.proxy.provider;
next.ipProxyMode = normalizedState.services.proxy.mode; next.ipProxyMode = normalizedState.services.proxy.mode;
next.kiroRsUrl = kiroState.targets['kiro-rs'].baseUrl; next.kiroRsUrl = kiroState.targets['kiro-rs']?.baseUrl || '';
next.kiroRsKey = kiroState.targets['kiro-rs'].apiKey; next.kiroRsKey = kiroState.targets['kiro-rs']?.apiKey || '';
next.grokWebchat2ApiUrl = grokState.targets.webchat2api?.baseUrl || '';
next.grokWebchat2ApiAdminKey = grokState.targets.webchat2api?.apiKey || '';
next.stepExecutionRangeByFlow = buildStepExecutionRangeByFlow(normalizedState); next.stepExecutionRangeByFlow = buildStepExecutionRangeByFlow(normalizedState);
next.settingsSchemaVersion = normalizedState.schemaVersion; next.settingsSchemaVersion = normalizedState.schemaVersion;
next.settingsState = cloneValue(normalizedState); next.settingsState = cloneValue(normalizedState);
+1 -2
View File
@@ -49,7 +49,6 @@ flowCapabilities.openai = {
supportsContributionMode: true, supportsContributionMode: true,
supportsPlatformBinding: ['cpa', 'sub2api', 'codex2api'], supportsPlatformBinding: ['cpa', 'sub2api', 'codex2api'],
supportsLuckmail: true, supportsLuckmail: true,
supportsOauthTimeoutBudget: true,
stepDefinitionMode: 'openai-dynamic', stepDefinitionMode: 'openai-dynamic',
}; };
@@ -61,7 +60,6 @@ flowCapabilities.siteA = {
supportsContributionMode: false, supportsContributionMode: false,
supportsPlatformBinding: ['siteA-panel'], supportsPlatformBinding: ['siteA-panel'],
supportsLuckmail: false, supportsLuckmail: false,
supportsOauthTimeoutBudget: false,
stepDefinitionMode: 'siteA', stepDefinitionMode: 'siteA',
}; };
``` ```
@@ -82,6 +80,7 @@ panelCapabilities = {
cpa: { cpa: {
supportsPhoneSignup: true, supportsPhoneSignup: true,
requiresPhoneSignupWarning: true, requiresPhoneSignupWarning: true,
usesOauthTimeoutBudget: true,
}, },
sub2api: { sub2api: {
supportsPhoneSignup: true, 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', id: 'kiro',
path: 'flows/kiro/', path: 'flows/kiro/',
}, },
grok: {
id: 'grok',
path: 'flows/grok/',
},
}); });
function normalizeFlowId(value = '') { function normalizeFlowId(value = '') {
@@ -32,10 +36,14 @@
...baseEntry, ...baseEntry,
definition: normalized === 'openai' definition: normalized === 'openai'
? (rootScope.MultiPageOpenAiFlowDefinition || null) ? (rootScope.MultiPageOpenAiFlowDefinition || null)
: (rootScope.MultiPageKiroFlowDefinition || null), : (normalized === 'kiro'
? (rootScope.MultiPageKiroFlowDefinition || null)
: (rootScope.MultiPageGrokFlowDefinition || null)),
workflow: normalized === 'openai' workflow: normalized === 'openai'
? (rootScope.MultiPageOpenAiWorkflow || null) ? (rootScope.MultiPageOpenAiWorkflow || null)
: (rootScope.MultiPageKiroWorkflow || null), : (normalized === 'kiro'
? (rootScope.MultiPageKiroWorkflow || null)
: (rootScope.MultiPageGrokWorkflow || null)),
}; };
} }
+20 -206
View File
@@ -14,43 +14,6 @@
'https://app.kiro.dev/*', 'https://app.kiro.dev/*',
'https://kiro.dev/*', 'https://kiro.dev/*',
]); ]);
const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([
Object.freeze({
source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '^\\s*(\\d{6})\\s*$',
flags: 'gm',
}),
Object.freeze({
source: '>\\s*(\\d{6})\\s*<',
flags: 'g',
}),
]);
const KIRO_AWS_SENDER_FILTERS = Object.freeze([
'no-reply@signin.aws',
'no-reply@login.awsapps.com',
'noreply@amazon.com',
'account-update@amazon.com',
'no-reply@aws.amazon.com',
'noreply@aws.amazon.com',
'aws',
]);
const KIRO_AWS_SUBJECT_FILTERS = Object.freeze([
'aws builder id',
'verification',
'验证码',
'code',
'aws',
]);
const KIRO_AWS_REQUIRED_KEYWORDS = Object.freeze([
'verification',
'验证码',
'code',
'aws',
]);
function isPlainObject(value) { function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value); return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
} }
@@ -375,32 +338,17 @@
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null), chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
completeNodeFromBackground, completeNodeFromBackground,
ensureContentScriptReadyOnTab = null, ensureContentScriptReadyOnTab = null,
ensureIcloudMailSession = null,
ensureMail2925MailboxSession = null,
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null, fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
getMailConfig = null,
getState = async () => ({}), getState = async () => ({}),
getTabId = async () => null, getTabId = async () => null,
HOTMAIL_PROVIDER = 'hotmail-api',
LUCKMAIL_PROVIDER = 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER = 'cloudmail',
YYDS_MAIL_PROVIDER = 'yyds-mail',
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
isTabAlive = async () => false, isTabAlive = async () => false,
maybeSubmitFlowContribution = async () => null, maybeSubmitFlowContribution = async () => null,
KIRO_REGISTER_INJECT_FILES = null, KIRO_REGISTER_INJECT_FILES = null,
KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = null, KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = null,
pollCloudflareTempEmailVerificationCode = null, pollFlowVerificationCode = null,
pollCloudMailVerificationCode = null,
pollHotmailVerificationCode = null,
pollLuckmailVerificationCode = null,
pollYydsMailVerificationCode = null,
registerTab = async () => {}, registerTab = async () => {},
reuseOrCreateTab = async () => null, reuseOrCreateTab = async () => null,
sendToContentScriptResilient = null, sendToContentScriptResilient = null,
sendToMailContentScriptResilient = null,
setState = async () => {}, setState = async () => {},
sleepWithStop = async (ms) => { sleepWithStop = async (ms) => {
await new Promise((resolve) => setTimeout(resolve, ms)); await new Promise((resolve) => setTimeout(resolve, ms));
@@ -658,44 +606,6 @@
return password; return password;
} }
function getExpectedMail2925MailboxEmail(state = {}) {
if (Boolean(state?.mail2925UseAccountPool)) {
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
const accounts = Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
const currentAccount = accounts.find((account) => String(account?.id || '') === currentAccountId) || null;
const accountEmail = String(currentAccount?.email || '').trim().toLowerCase();
if (accountEmail) {
return accountEmail;
}
}
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
}
async function focusOrOpenMailTab(mail) {
if (!mail?.source) {
return;
}
const alive = await isTabAlive(mail.source);
if (alive) {
if (mail.navigateOnReuse) {
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
return;
}
const tabId = await getTabId(mail.source);
if (Number.isInteger(tabId)) {
await activateTab(tabId);
}
return;
}
await reuseOrCreateTab(mail.source, mail.url, {
inject: mail.inject,
injectSource: mail.injectSource,
});
}
async function collectKiroWebSessionTabs(currentState = {}) { async function collectKiroWebSessionTabs(currentState = {}) {
const runtimeState = readKiroRuntime(currentState); const runtimeState = readKiroRuntime(currentState);
const candidates = []; const candidates = [];
@@ -901,131 +811,35 @@
throw new Error(`Kiro Web 登录态尚未建立。请在自动打开的 Kiro 账号页登录后,从步骤 7 继续。${detail}`); throw new Error(`Kiro Web 登录态尚未建立。请在自动打开的 Kiro 账号页登录后,从步骤 7 继续。${detail}`);
} }
function buildDesktopOtpPollPayload(step, state = {}, mail = {}, filterAfterTimestamp = 0) {
const runtimeState = readKiroRuntime(state);
const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase();
const targetEmailHints = targetEmail ? [targetEmail] : [];
const isMail2925Provider = String(mail?.provider || '').trim().toLowerCase() === '2925';
const normalizedProvider = String(mail?.provider || '').trim().toLowerCase();
const maxAttempts = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
? 3
: (isMail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5);
const intervalMs = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase()
? 15000
: (isMail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000);
return {
flowId: 'kiro',
step,
targetEmail,
targetEmailHints,
filterAfterTimestamp,
senderFilters: [...KIRO_AWS_SENDER_FILTERS],
subjectFilters: [...KIRO_AWS_SUBJECT_FILTERS],
requiredKeywords: [...KIRO_AWS_REQUIRED_KEYWORDS],
codePatterns: [...KIRO_AWS_VERIFICATION_CODE_PATTERNS],
mail2925MatchTargetEmail: isMail2925Provider
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive',
maxAttempts,
intervalMs,
};
}
function getMailPollingResponseTimeoutMs(payload = {}) {
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
return Math.max(45000, maxAttempts * intervalMs + 25000);
}
async function pollDesktopOtpCode(step, state = {}, nodeId = '') { async function pollDesktopOtpCode(step, state = {}, nodeId = '') {
if (typeof getMailConfig !== 'function') { if (typeof pollFlowVerificationCode !== 'function') {
throw new Error('Kiro 桌面授权验证码步骤缺少邮箱配置能力,无法继续执行。'); throw new Error('Kiro 桌面授权验证码步骤缺少共享邮件轮询能力,无法继续执行。');
}
const mail = getMailConfig(state);
if (mail?.error) {
throw new Error(mail.error);
} }
const runtimeState = readKiroRuntime(state); const runtimeState = readKiroRuntime(state);
const requestedAt = Math.max(0, Number(runtimeState.desktopAuth?.otpRequestedAt) || Date.now()); const requestedAt = Math.max(0, Number(runtimeState.desktopAuth?.otpRequestedAt) || Date.now());
const filterAfterTimestamp = mail.provider === '2925' const mailProvider = cleanString(state?.mailProvider).toLowerCase();
const filterAfterTimestamp = mailProvider === '2925'
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS) ? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: requestedAt; : requestedAt;
const pollPayload = buildDesktopOtpPollPayload(step, state, mail, filterAfterTimestamp);
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') { return pollFlowVerificationCode({
await log(`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`, 'info', nodeId); actionLabel: '桌面授权验证码',
await ensureIcloudMailSession({ filterAfterTimestamp,
state, flowId: 'kiro',
step,
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,
},
{
timeoutMs: responseTimeoutMs,
responseTimeoutMs,
maxRecoveryAttempts: 2,
logStep: step, logStep: step,
logStepKey: 'kiro-complete-desktop-authorize', logStepKey: 'kiro-complete-desktop-authorize',
} missingCapabilityMessage: 'Kiro 桌面授权验证码步骤缺少共享邮件轮询能力,无法继续执行。',
); nodeId: 'kiro-complete-desktop-authorize',
notFoundMessage: `步骤 ${step}:邮箱轮询结束,但未获取到桌面授权验证码。`,
if (result?.error) { state: {
throw new Error(result.error); ...state,
} activeFlowId: 'kiro',
if (!result?.code) { flowId: 'kiro',
throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到桌面授权验证码。`); visibleStep: step,
} },
return result; step,
});
} }
async function executeKiroStartDesktopAuthorize(state = {}) { async function executeKiroStartDesktopAuthorize(state = {}) {
+20 -208
View File
@@ -32,42 +32,6 @@
'https://profile.aws.amazon.com', 'https://profile.aws.amazon.com',
]); ]);
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000; const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([
Object.freeze({
source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})',
flags: 'gi',
}),
Object.freeze({
source: '^\\s*(\\d{6})\\s*$',
flags: 'gm',
}),
Object.freeze({
source: '>\\s*(\\d{6})\\s*<',
flags: 'g',
}),
]);
const KIRO_AWS_SENDER_FILTERS = Object.freeze([
'no-reply@signin.aws',
'no-reply@login.awsapps.com',
'noreply@amazon.com',
'account-update@amazon.com',
'no-reply@aws.amazon.com',
'noreply@aws.amazon.com',
'aws',
]);
const KIRO_AWS_SUBJECT_FILTERS = Object.freeze([
'aws builder id',
'verification',
'验证码',
'code',
'aws',
]);
const KIRO_AWS_REQUIRED_KEYWORDS = Object.freeze([
'verification',
'验证码',
'code',
'aws',
]);
const KIRO_REGISTER_PAGE_STATES = Object.freeze([ const KIRO_REGISTER_PAGE_STATES = Object.freeze([
'kiro_signin_page', 'kiro_signin_page',
'email_entry', 'email_entry',
@@ -387,32 +351,17 @@
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null), chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
completeNodeFromBackground, completeNodeFromBackground,
ensureContentScriptReadyOnTab = null, ensureContentScriptReadyOnTab = null,
ensureIcloudMailSession = null,
ensureMail2925MailboxSession = null,
generatePassword = null, generatePassword = null,
generateRandomName = null, generateRandomName = null,
getMailConfig = null,
getState = async () => ({}), getState = async () => ({}),
getTabId = async () => null, getTabId = async () => null,
HOTMAIL_PROVIDER = 'hotmail-api',
LUCKMAIL_PROVIDER = 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER = 'cloudmail',
YYDS_MAIL_PROVIDER = 'yyds-mail',
MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
isTabAlive = async () => false, isTabAlive = async () => false,
KIRO_REGISTER_INJECT_FILES = null, KIRO_REGISTER_INJECT_FILES = null,
pollCloudflareTempEmailVerificationCode = null, pollFlowVerificationCode = null,
pollCloudMailVerificationCode = null,
pollHotmailVerificationCode = null,
pollLuckmailVerificationCode = null,
pollYydsMailVerificationCode = null,
registerTab = async () => {}, registerTab = async () => {},
resolveSignupEmailForFlow = null, resolveSignupEmailForFlow = null,
reuseOrCreateTab = async () => null, reuseOrCreateTab = async () => null,
sendToContentScriptResilient = null, sendToContentScriptResilient = null,
sendToMailContentScriptResilient = null,
setPasswordState = async () => {}, setPasswordState = async () => {},
setState = async () => {}, setState = async () => {},
sleepWithStop = async (ms) => { sleepWithStop = async (ms) => {
@@ -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 = '') { async function pollKiroVerificationCode(step, state = {}, nodeId = '') {
if (typeof getMailConfig !== 'function') { if (typeof pollFlowVerificationCode !== 'function') {
throw new Error('Kiro 验证码步骤缺少邮箱配置能力,无法继续执行。'); throw new Error('Kiro 验证码步骤缺少共享邮件轮询能力,无法继续执行。');
}
const mail = getMailConfig(state);
if (mail?.error) {
throw new Error(mail.error);
} }
const runtimeState = readKiroRuntime(state); const runtimeState = readKiroRuntime(state);
const recordedRequestedAt = Math.max(0, Number(runtimeState.register?.verificationRequestedAt) || 0); const recordedRequestedAt = Math.max(0, Number(runtimeState.register?.verificationRequestedAt) || 0);
const requestedAt = recordedRequestedAt || Math.max(0, Date.now() - MAIL_2925_FILTER_LOOKBACK_MS); const requestedAt = recordedRequestedAt || Math.max(0, Date.now() - MAIL_2925_FILTER_LOOKBACK_MS);
const filterAfterTimestamp = mail.provider === '2925' const mailProvider = cleanString(state?.mailProvider).toLowerCase();
const filterAfterTimestamp = mailProvider === '2925'
? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS) ? Math.max(0, requestedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: requestedAt; : requestedAt;
const pollPayload = buildKiroVerificationPollPayload(step, state, mail, filterAfterTimestamp);
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') { return pollFlowVerificationCode({
await log(`步骤 ${step}:正在确认 ${mail.label || 'iCloud 邮箱'} 登录状态...`, 'info', nodeId); actionLabel: 'Kiro 验证码',
await ensureIcloudMailSession({ filterAfterTimestamp,
state, flowId: 'kiro',
step,
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,
},
{
timeoutMs: responseTimeoutMs,
responseTimeoutMs,
maxRecoveryAttempts: 2,
logStep: step, logStep: step,
logStepKey: 'kiro-submit-verification-code', logStepKey: 'kiro-submit-verification-code',
} missingCapabilityMessage: 'Kiro 验证码步骤缺少共享邮件轮询能力,无法继续执行。',
); nodeId: 'kiro-submit-verification-code',
notFoundMessage: `步骤 ${step}:邮箱轮询结束,但未获取到 Kiro 验证码。`,
if (result?.error) { state: {
throw new Error(result.error); ...state,
} activeFlowId: 'kiro',
if (!result?.code) { flowId: 'kiro',
throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`); visibleStep: step,
} },
return result; step,
});
} }
async function executeKiroOpenRegisterPage(state = {}) { async function executeKiroOpenRegisterPage(state = {}) {
+7 -2
View File
@@ -34,18 +34,23 @@
"kiro-rs" "kiro-rs"
], ],
"supportsLuckmail": false, "supportsLuckmail": false,
"supportsOauthTimeoutBudget": false,
"canSwitchFlow": true, "canSwitchFlow": true,
"stepDefinitionMode": "kiro", "stepDefinitionMode": "kiro",
"targetSelectorLabel": "来源" "targetSelectorLabel": "来源"
}, },
"baseGroups": [ "baseGroups": [
"kiro-runtime-status" "kiro-runtime-status",
"shared-auto-run",
"shared-settings-actions"
], ],
"targets": { "targets": {
"kiro-rs": { "kiro-rs": {
"id": "kiro-rs", "id": "kiro-rs",
"label": "kiro.rs", "label": "kiro.rs",
"defaultState": {
"baseUrl": "",
"apiKey": ""
},
"groups": [ "groups": [
"kiro-target-kiro-rs" "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 // 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 = { const payload = {
profileSubmitted: true, profileSubmitted: true,
postSubmitChecked: true, postSubmitChecked: !navigationStarted,
}; };
if (isAgeMode) { if (isAgeMode) {
payload.ageMode = true; payload.ageMode = true;
} }
if (navigationStarted) { if (navigationStarted) {
payload.navigationStarted = true; 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) { if (outcome?.state) {
payload.postSubmitChecked = true;
payload.outcome = outcome.state; payload.outcome = outcome.state;
} }
if (outcome?.url) { if (outcome?.url) {
@@ -6825,6 +6834,7 @@ function installStep5NavigationCompletionReporter(completeOnce) {
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') { if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
return () => {}; return () => {};
} }
let reportedNavigation = false;
const debugLog = typeof logStep5SubmitDebug === 'function' const debugLog = typeof logStep5SubmitDebug === 'function'
? logStep5SubmitDebug ? logStep5SubmitDebug
: (message, options = {}) => { : (message, options = {}) => {
@@ -6838,6 +6848,25 @@ function installStep5NavigationCompletionReporter(completeOnce) {
const onNavigationStarted = (event) => { const onNavigationStarted = (event) => {
const eventType = String(event?.type || 'navigation').trim() || 'navigation'; 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})。`, { debugLog(`检测到页面开始导航(event=${eventType})。`, {
level: 'warn', level: 'warn',
}); });
@@ -7240,13 +7269,18 @@ async function step5_fillNameBirthday(payload) {
const completionPayload = getStep5DirectCompletionPayload({ const completionPayload = getStep5DirectCompletionPayload({
isAgeMode, isAgeMode,
navigationStarted: Boolean(extra.navigationStarted), navigationStarted: Boolean(extra.navigationStarted),
navigationEventType: extra.navigationEventType || '',
outcome: extra.outcome || null, 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})。`, { debugLog(`准备发送完成信号(reason=${completionReason}isAgeMode=${isAgeMode})。`, {
level: extra?.navigationStarted ? 'warn' : 'info', level: extra?.navigationStarted ? 'warn' : 'info',
}); });
reportedCompletionPayload = completionPayload;
reportComplete(5, completionPayload);
return completionPayload; return completionPayload;
} }
+49 -4
View File
@@ -38,7 +38,6 @@
"codex2api" "codex2api"
], ],
"supportsLuckmail": true, "supportsLuckmail": true,
"supportsOauthTimeoutBudget": true,
"canSwitchFlow": true, "canSwitchFlow": true,
"stepDefinitionMode": "openai-dynamic", "stepDefinitionMode": "openai-dynamic",
"targetSelectorLabel": "来源" "targetSelectorLabel": "来源"
@@ -46,13 +45,20 @@
"baseGroups": [ "baseGroups": [
"openai-plus", "openai-plus",
"openai-phone", "openai-phone",
"shared-auto-run",
"openai-oauth", "openai-oauth",
"openai-step6" "openai-step6",
"shared-settings-actions"
], ],
"targets": { "targets": {
"cpa": { "cpa": {
"id": "cpa", "id": "cpa",
"label": "CPA 面板", "label": "CPA 面板",
"defaultState": {
"vpsUrl": "",
"vpsPassword": "",
"localCpaStep9Mode": "submit"
},
"groups": [ "groups": [
"openai-target-cpa" "openai-target-cpa"
] ]
@@ -60,6 +66,18 @@
"sub2api": { "sub2api": {
"id": "sub2api", "id": "sub2api",
"label": "SUB2API", "label": "SUB2API",
"defaultState": {
"sub2apiUrl": "",
"sub2apiEmail": "",
"sub2apiPassword": "",
"sub2apiGroupName": "codex",
"sub2apiGroupNames": [
"codex",
"openai-plus"
],
"sub2apiAccountPriority": 1,
"sub2apiDefaultProxyName": ""
},
"groups": [ "groups": [
"openai-target-sub2api" "openai-target-sub2api"
] ]
@@ -67,11 +85,37 @@
"codex2api": { "codex2api": {
"id": "codex2api", "id": "codex2api",
"label": "Codex2API", "label": "Codex2API",
"defaultState": {
"codex2apiUrl": "",
"codex2apiAdminKey": ""
},
"groups": [ "groups": [
"openai-target-codex2api" "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": { "runtimeSources": {
"openai-auth": { "openai-auth": {
"flowId": "openai", "flowId": "openai",
@@ -382,8 +426,8 @@
"id": "openai-oauth", "id": "openai-oauth",
"label": "OAuth", "label": "OAuth",
"rowIds": [ "rowIds": [
"row-oauth-flow-timeout", "row-oauth-display",
"row-oauth-display" "row-oauth-callback"
] ]
}, },
"openai-step6": { "openai-step6": {
@@ -398,6 +442,7 @@
"cpa": { "cpa": {
"supportsPhoneSignup": true, "supportsPhoneSignup": true,
"requiresPhoneSignupWarning": true, "requiresPhoneSignupWarning": true,
"usesOauthTimeoutBudget": true,
"supportedPlusAccountAccessStrategies": [ "supportedPlusAccountAccessStrategies": [
"oauth", "oauth",
"cpa_codex_session" "cpa_codex_session"
+18
View File
@@ -3027,11 +3027,19 @@
'paypal-hosted-review', 'paypal-hosted-review',
'gopay-subscription-confirm', 'gopay-subscription-confirm',
]); ]);
const POST_LOGIN_PHONE_VERIFICATION_STEP_KEYS = Object.freeze([
'post-login-phone-verification',
'post-bound-email-phone-verification',
]);
function omitPlusPaymentChainSteps(steps = []) { function omitPlusPaymentChainSteps(steps = []) {
return steps.filter((step) => !PLUS_PAYMENT_CHAIN_STEP_KEYS.includes(String(step?.key || '').trim())); 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 = []) { function reindexModeStepDefinitions(steps = []) {
return (Array.isArray(steps) ? steps : []).map((step, index) => ({ return (Array.isArray(steps) ? steps : []).map((step, index) => ({
...step, ...step,
@@ -3116,6 +3124,13 @@
return Boolean(options?.phoneSignupReloginAfterBindEmailEnabled); return Boolean(options?.phoneSignupReloginAfterBindEmailEnabled);
} }
function isPhoneVerificationEnabled(options = {}) {
if (Object.prototype.hasOwnProperty.call(options || {}, 'phoneVerificationEnabled')) {
return Boolean(options.phoneVerificationEnabled);
}
return true;
}
function normalizePlusAccountAccessStrategy(value = '') { function normalizePlusAccountAccessStrategy(value = '') {
const normalized = String(value || '').trim().toLowerCase(); const normalized = String(value || '').trim().toLowerCase();
if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) { if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
@@ -3206,6 +3221,9 @@
) { ) {
steps = omitPlusPaymentChainSteps(steps); steps = omitPlusPaymentChainSteps(steps);
} }
if (!isPhoneVerificationEnabled(options)) {
steps = omitPostLoginPhoneVerificationSteps(steps);
}
return reindexModeStepDefinitions(steps); return reindexModeStepDefinitions(steps);
} }
+7 -2
View File
@@ -1,8 +1,8 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "FlowPilot", "name": "FlowPilot",
"version": "2.4", "version": "2.5",
"version_name": "FlowPilot2.4", "version_name": "FlowPilot2.5",
"description": "用于自动执行多步骤 OAuth 注册流程", "description": "用于自动执行多步骤 OAuth 注册流程",
"permissions": [ "permissions": [
"sidePanel", "sidePanel",
@@ -51,6 +51,7 @@
"content/activation-utils.js", "content/activation-utils.js",
"flows/openai/index.js", "flows/openai/index.js",
"flows/kiro/index.js", "flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js", "flows/index.js",
"core/flow-kernel/flow-registry.js", "core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js", "core/flow-kernel/source-registry.js",
@@ -72,6 +73,7 @@
"content/activation-utils.js", "content/activation-utils.js",
"flows/openai/index.js", "flows/openai/index.js",
"flows/kiro/index.js", "flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js", "flows/index.js",
"core/flow-kernel/flow-registry.js", "core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js", "core/flow-kernel/source-registry.js",
@@ -93,6 +95,7 @@
"content/activation-utils.js", "content/activation-utils.js",
"flows/openai/index.js", "flows/openai/index.js",
"flows/kiro/index.js", "flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js", "flows/index.js",
"core/flow-kernel/flow-registry.js", "core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js", "core/flow-kernel/source-registry.js",
@@ -111,6 +114,7 @@
"content/activation-utils.js", "content/activation-utils.js",
"flows/openai/index.js", "flows/openai/index.js",
"flows/kiro/index.js", "flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js", "flows/index.js",
"core/flow-kernel/flow-registry.js", "core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js", "core/flow-kernel/source-registry.js",
@@ -128,6 +132,7 @@
"content/activation-utils.js", "content/activation-utils.js",
"flows/openai/index.js", "flows/openai/index.js",
"flows/kiro/index.js", "flows/kiro/index.js",
"flows/grok/index.js",
"flows/index.js", "flows/index.js",
"core/flow-kernel/flow-registry.js", "core/flow-kernel/flow-registry.js",
"core/flow-kernel/source-registry.js", "core/flow-kernel/source-registry.js",
+32 -2
View File
@@ -191,15 +191,44 @@
return Boolean(normalizedAdapterId) && getContributionAdapterIds(flowId).includes(normalizedAdapterId); return Boolean(normalizedAdapterId) && getContributionAdapterIds(flowId).includes(normalizedAdapterId);
} }
function assertPublishedFlowsHaveContributionAdapters(flowIds = undefined) { function hasPublicationTargets(flowId = DEFAULT_FLOW_ID) {
if (typeof flowRegistryApi.getPublicationTargetDefinitions !== 'function') {
return false;
}
const definitions = flowRegistryApi.getPublicationTargetDefinitions(flowId) || {};
return Object.keys(definitions).length > 0;
}
function expectsContributionAdapter(flowId = DEFAULT_FLOW_ID) {
const normalizedFlowId = normalizeFlowId(flowId, DEFAULT_FLOW_ID);
const capabilities = typeof flowRegistryApi.getFlowCapabilities === 'function'
? (flowRegistryApi.getFlowCapabilities(normalizedFlowId) || {})
: {};
return Boolean(
hasPublicationTargets(normalizedFlowId)
|| capabilities.supportsAccountContribution
|| capabilities.supportsContributionMode
|| getContributionAdapterIds(normalizedFlowId).length
);
}
function getPublishedContributionFlowIds(flowIds = undefined) {
const ids = Array.isArray(flowIds) const ids = Array.isArray(flowIds)
? flowIds ? flowIds
: (typeof flowRegistryApi.getRegisteredFlowIds === 'function' : (typeof flowRegistryApi.getRegisteredFlowIds === 'function'
? flowRegistryApi.getRegisteredFlowIds() ? flowRegistryApi.getRegisteredFlowIds()
: Object.keys(FLOW_ADAPTER_IDS)); : Object.keys(FLOW_ADAPTER_IDS));
const missing = ids return ids
.map((flowId) => normalizeString(flowId).toLowerCase()) .map((flowId) => normalizeString(flowId).toLowerCase())
.filter(Boolean) .filter(Boolean)
.filter((flowId) => expectsContributionAdapter(flowId));
}
function assertPublishedFlowsHaveContributionAdapters(flowIds = undefined) {
const ids = Array.isArray(flowIds)
? flowIds.map((flowId) => normalizeString(flowId).toLowerCase()).filter(Boolean)
: getPublishedContributionFlowIds();
const missing = ids
.filter((flowId) => getContributionAdapterIds(flowId).length === 0); .filter((flowId) => getContributionAdapterIds(flowId).length === 0);
if (missing.length) { if (missing.length) {
throw new Error(`缺少账号贡献适配器:${missing.join(', ')}`); throw new Error(`缺少账号贡献适配器:${missing.join(', ')}`);
@@ -217,6 +246,7 @@
getContributionAdapterIds, getContributionAdapterIds,
getContributionAdapters, getContributionAdapters,
getDefaultContributionAdapterId, getDefaultContributionAdapterId,
getPublishedContributionFlowIds,
hasContributionAdapter, hasContributionAdapter,
normalizeAdapterId, normalizeAdapterId,
normalizeFlowId, normalizeFlowId,
+1 -1
View File
@@ -1263,7 +1263,7 @@ function updateIpProxyUI(state = latestState) {
const isAccountMode = mode === 'account'; const isAccountMode = mode === 'account';
const showSessionOptions = isAccountMode && service === '711proxy'; const showSessionOptions = isAccountMode && service === '711proxy';
const hasAccountListConfigured = accountListAvailable && isAccountMode && hasCurrentInputAccountListEntries(); const hasAccountListConfigured = accountListAvailable && isAccountMode && hasCurrentInputAccountListEntries();
const canOperate = !isAutoRunLockedPhase() && !isAutoRunScheduledPhase(); const canOperate = !isAutoRunLockedPhase();
const actionState = getIpProxyActionState(); const actionState = getIpProxyActionState();
const actionBusy = Boolean(actionState.busy); const actionBusy = Boolean(actionState.busy);
const busyAction = normalizeIpProxyActionType(actionState.action); const busyAction = normalizeIpProxyActionType(actionState.action);
+19 -34
View File
@@ -1268,6 +1268,19 @@ header {
width: 76px; 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 { #row-codex2api-url .data-label {
font-size: 10px; font-size: 10px;
letter-spacing: 0.02em; letter-spacing: 0.02em;
@@ -2197,15 +2210,6 @@ header {
justify-content: flex-end; 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 { .step6-cookie-cleanup-setting {
gap: 8px; gap: 8px;
} }
@@ -2227,25 +2231,6 @@ header {
text-align: center; 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 { .setting-caption-left {
text-align: left; text-align: left;
} }
@@ -2720,7 +2705,7 @@ header {
.auto-continue-bar svg { color: var(--orange); flex-shrink: 0; } .auto-continue-bar svg { color: var(--orange); flex-shrink: 0; }
.auto-hint { font-size: 13px; color: var(--orange); flex: 1; font-weight: 500; } .auto-hint { font-size: 13px; color: var(--orange); flex: 1; font-weight: 500; }
.auto-schedule-bar { .auto-countdown-bar {
margin-top: 8px; margin-top: 8px;
padding: 10px; padding: 10px;
background: var(--blue-soft); background: var(--blue-soft);
@@ -2732,12 +2717,12 @@ header {
gap: 10px; gap: 10px;
} }
.auto-schedule-bar svg { .auto-countdown-bar svg {
color: var(--blue); color: var(--blue);
flex-shrink: 0; flex-shrink: 0;
} }
.auto-schedule-copy { .auto-countdown-copy {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
@@ -2745,19 +2730,19 @@ header {
min-width: 0; min-width: 0;
} }
.auto-schedule-title { .auto-countdown-title {
font-size: 13px; font-size: 13px;
font-weight: 700; font-weight: 700;
color: var(--blue); color: var(--blue);
} }
.auto-schedule-meta { .auto-countdown-meta {
font-size: 12px; font-size: 12px;
line-height: 1.5; line-height: 1.5;
color: var(--text-secondary); color: var(--text-secondary);
} }
.auto-schedule-actions { .auto-countdown-actions {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
+59 -45
View File
@@ -121,20 +121,24 @@
<section id="data-section"> <section id="data-section">
<div id="settings-card" class="data-card"> <div id="settings-card" class="data-card">
<div class="data-row"> <div class="data-row" id="row-flow-selector">
<span class="data-label">注册</span> <span class="data-label">注册</span>
<select id="select-flow" class="data-select" aria-label="Flow 选择"> <select id="select-flow" class="data-select" aria-label="Flow 选择">
<option value="openai" selected>Codex / OpenAI</option> <option value="openai" selected>Codex / OpenAI</option>
<option value="kiro">Kiro</option> <option value="kiro">Kiro</option>
<option value="grok">Grok</option>
</select> </select>
</div> </div>
<div class="data-row"> <div class="data-row" id="row-source-selector">
<span id="label-source-selector" class="data-label">来源</span> <span id="label-source-selector" class="data-label">来源</span>
<div class="data-inline">
<select id="select-panel-mode" class="data-select"> <select id="select-panel-mode" class="data-select">
<option value="cpa">CPA 面板</option> <option value="cpa">CPA 面板</option>
<option value="sub2api">SUB2API</option> <option value="sub2api">SUB2API</option>
<option value="codex2api">Codex2API</option> <option value="codex2api">Codex2API</option>
</select> </select>
<button id="btn-open-webchat2api-github" class="btn btn-ghost btn-xs data-inline-btn" type="button">GitHub</button>
</div>
</div> </div>
<div id="contribution-mode-panel" class="contribution-mode-panel" hidden> <div id="contribution-mode-panel" class="contribution-mode-panel" hidden>
<div class="contribution-mode-panel-header"> <div class="contribution-mode-panel-header">
@@ -290,6 +294,42 @@
<span class="data-label">上传状态</span> <span class="data-label">上传状态</span>
<span id="display-kiro-upload-status" class="data-value mono">未开始</span> <span id="display-kiro-upload-status" class="data-value mono">未开始</span>
</div> </div>
<div class="data-row" id="row-grok-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"> <div class="data-row module-divider-start" id="row-custom-password">
<span class="data-label">账户密码</span> <span class="data-label">账户密码</span>
<div class="input-with-icon"> <div class="input-with-icon">
@@ -657,26 +697,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="data-row module-divider-start" id="row-auto-delay-settings"> <div class="data-row module-divider-start" id="row-shared-auto-run">
<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">
<span class="data-label">自动重试</span> <span class="data-label">自动重试</span>
<div class="data-inline setting-pair"> <div class="data-inline setting-pair">
<div class="setting-group setting-group-primary"> <div class="setting-group setting-group-primary">
@@ -697,22 +718,9 @@
</div> </div>
</div> </div>
</div> </div>
<div class="data-row" id="row-oauth-flow-timeout"> <div class="data-row" id="row-auto-run-thread-interval">
<span class="data-label">授权总超时</span> <span class="data-label">线程间隔</span>
<div class="data-inline setting-pair"> <div class="data-inline">
<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"> <div class="setting-controls">
<input type="number" id="input-auto-skip-failures-thread-interval-minutes" <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="兜底模式下,两轮线程之间的等待分钟数" /> class="data-input auto-delay-input" value="0" min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" />
@@ -720,7 +728,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="data-row" id="row-step-execution-range"> <div class="data-row" id="row-step-execution-range">
<span class="data-label">执行范围</span> <span class="data-label">执行范围</span>
<div class="data-inline setting-pair step-execution-range-setting"> <div class="data-inline setting-pair step-execution-range-setting">
@@ -745,10 +752,15 @@
<span class="data-label">OAuth</span> <span class="data-label">OAuth</span>
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span> <span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
</div> </div>
<div class="data-row"> <div class="data-row" id="row-oauth-callback">
<span class="data-label">回调</span> <span class="data-label">回调</span>
<div class="data-inline data-value-actions"> <div class="data-inline data-value-actions">
<span id="display-localhost-url" class="data-value data-value-fill mono truncate">等待中...</span> <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> <button id="btn-save-settings" class="btn btn-outline btn-sm data-inline-btn" type="button">保存</button>
</div> </div>
</div> </div>
@@ -1761,16 +1773,16 @@
<span class="auto-hint">先自动获取邮箱,或手动粘贴邮箱后再继续</span> <span class="auto-hint">先自动获取邮箱,或手动粘贴邮箱后再继续</span>
<button id="btn-auto-continue" class="btn btn-primary btn-sm">继续</button> <button id="btn-auto-continue" class="btn btn-primary btn-sm">继续</button>
</div> </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"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" /> <polyline points="12 6 12 12 16 14" />
</svg> </svg>
<div class="auto-schedule-copy"> <div class="auto-countdown-copy">
<span id="auto-schedule-title" class="auto-schedule-title">已计划自动运行</span> <span id="auto-countdown-title" class="auto-countdown-title">等待继续</span>
<span id="auto-schedule-meta" class="auto-schedule-meta">等待倒计时开始...</span> <span id="auto-countdown-meta" class="auto-countdown-meta">等待倒计时开始...</span>
</div> </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-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> <button id="btn-auto-cancel-schedule" class="btn btn-outline btn-sm" type="button">取消</button>
</div> </div>
@@ -1881,6 +1893,8 @@
<script src="../flows/openai/workflow.js"></script> <script src="../flows/openai/workflow.js"></script>
<script src="../flows/kiro/index.js"></script> <script src="../flows/kiro/index.js"></script>
<script src="../flows/kiro/workflow.js"></script> <script src="../flows/kiro/workflow.js"></script>
<script src="../flows/grok/index.js"></script>
<script src="../flows/grok/workflow.js"></script>
<script src="../flows/index.js"></script> <script src="../flows/index.js"></script>
<script src="../core/flow-kernel/flow-registry.js"></script> <script src="../core/flow-kernel/flow-registry.js"></script>
<script src="../shared/contribution-registry.js"></script> <script src="../shared/contribution-registry.js"></script>
+331 -172
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: '', customPassword: '',
autoRunSkipFailures: true, autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
mailProvider: '163', mailProvider: '163',
emailGenerator: 'duck', 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 }); events.broadcasts.push({ phase, ...payload });
currentState = { currentState = {
...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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun, autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns, 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; return sessionSeed;
}, },
getAutoRunStatusPayload: (phase, 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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1, autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -297,8 +295,6 @@ test('auto-run controller treats phone-number supply exhaustion as round-fatal a
customPassword: '', customPassword: '',
autoRunSkipFailures: true, autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
mailProvider: '163', mailProvider: '163',
emailGenerator: 'duck', emailGenerator: 'duck',
@@ -348,7 +344,7 @@ test('auto-run controller treats phone-number supply exhaustion as round-fatal a
events.broadcasts.push({ phase, ...payload }); events.broadcasts.push({ phase, ...payload });
currentState = { currentState = {
...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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun, autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns, 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; return sessionSeed;
}, },
getAutoRunStatusPayload: (phase, 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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1, autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -460,8 +456,6 @@ test('auto-run controller treats ended GPC task as round-fatal and skips same-ro
customPassword: '', customPassword: '',
autoRunSkipFailures: true, autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
mailProvider: '163', mailProvider: '163',
emailGenerator: 'duck', 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 }); events.broadcasts.push({ phase, ...payload });
currentState = { currentState = {
...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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun, autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns, 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; return sessionSeed;
}, },
getAutoRunStatusPayload: (phase, 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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1, autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -621,8 +615,6 @@ test('auto-run controller keeps same-round retrying for step9 local replacement
customPassword: '', customPassword: '',
autoRunSkipFailures: true, autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
mailProvider: '163', mailProvider: '163',
emailGenerator: 'duck', emailGenerator: 'duck',
@@ -672,7 +664,7 @@ test('auto-run controller keeps same-round retrying for step9 local replacement
events.broadcasts.push({ phase, ...payload }); events.broadcasts.push({ phase, ...payload });
currentState = { currentState = {
...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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun, autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns, autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
@@ -688,7 +680,7 @@ test('auto-run controller keeps same-round retrying for step9 local replacement
return sessionSeed; return sessionSeed;
}, },
getAutoRunStatusPayload: (phase, 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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1, autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -779,8 +771,6 @@ test('auto-run controller skips user_already_exists failures to the next round i
customPassword: '', customPassword: '',
autoRunSkipFailures: true, autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
mailProvider: '163', mailProvider: '163',
emailGenerator: 'duck', 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 }); events.broadcasts.push({ phase, ...payload });
currentState = { currentState = {
...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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun, autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns, 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; return sessionSeed;
}, },
getAutoRunStatusPayload: (phase, 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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1, autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -943,8 +933,6 @@ test('auto-run controller skips step 4 repeated 405 recovery failures to the nex
customPassword: '', customPassword: '',
autoRunSkipFailures: true, autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
mailProvider: '163', mailProvider: '163',
emailGenerator: 'duck', 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 }); events.broadcasts.push({ phase, ...payload });
currentState = { currentState = {
...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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun, autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns, 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; return sessionSeed;
}, },
getAutoRunStatusPayload: (phase, 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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1, autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -1108,8 +1096,6 @@ test('auto-run controller keeps retrying the same custom mail provider pool emai
customPassword: '', customPassword: '',
autoRunSkipFailures: true, autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
mailProvider: 'custom', mailProvider: 'custom',
customMailProviderPool: ['first@example.com'], 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 }); events.broadcasts.push({ phase, ...payload });
currentState = { currentState = {
...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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun, autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns, 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; return sessionSeed;
}, },
getAutoRunStatusPayload: (phase, 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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1, autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -1274,8 +1260,6 @@ test('auto-run controller retries 5sim rate limit failures instead of treating c
customPassword: '', customPassword: '',
autoRunSkipFailures: true, autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
mailProvider: '163', mailProvider: '163',
emailGenerator: 'duck', emailGenerator: 'duck',
@@ -1325,7 +1309,7 @@ test('auto-run controller retries 5sim rate limit failures instead of treating c
events.broadcasts.push({ phase, ...payload }); events.broadcasts.push({ phase, ...payload });
currentState = { currentState = {
...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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun, autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns, 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; return sessionSeed;
}, },
getAutoRunStatusPayload: (phase, 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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1, autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -130,8 +130,6 @@ let currentState = {
customPassword: '', customPassword: '',
autoRunSkipFailures: false, autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
signupMethod: 'phone', signupMethod: 'phone',
resolvedSignupMethod: 'phone', resolvedSignupMethod: 'phone',
@@ -204,8 +202,6 @@ async function resetState() {
customPassword: prev.customPassword, customPassword: prev.customPassword,
autoRunSkipFailures: prev.autoRunSkipFailures, autoRunSkipFailures: prev.autoRunSkipFailures,
autoRunFallbackThreadIntervalMinutes: prev.autoRunFallbackThreadIntervalMinutes, autoRunFallbackThreadIntervalMinutes: prev.autoRunFallbackThreadIntervalMinutes,
autoRunDelayEnabled: prev.autoRunDelayEnabled,
autoRunDelayMinutes: prev.autoRunDelayMinutes,
autoStepDelaySeconds: prev.autoStepDelaySeconds, autoStepDelaySeconds: prev.autoStepDelaySeconds,
signupMethod: prev.signupMethod, signupMethod: prev.signupMethod,
resolvedSignupMethod: null, resolvedSignupMethod: null,
+2 -4
View File
@@ -104,8 +104,6 @@ test('auto-run controller verifies hotmail mailbox before each fresh attempt sta
customPassword: '', customPassword: '',
autoRunSkipFailures: false, autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
mailProvider: 'hotmail-api', mailProvider: 'hotmail-api',
emailGenerator: 'duck', emailGenerator: 'duck',
@@ -149,7 +147,7 @@ test('auto-run controller verifies hotmail mailbox before each fresh attempt sta
broadcastAutoRunStatus: async (phase, payload = {}) => { broadcastAutoRunStatus: async (phase, payload = {}) => {
currentState = { currentState = {
...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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun, autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns, 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 }); events.preflightCalls.push({ ...payload });
}, },
getAutoRunStatusPayload: (phase, 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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1, 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, phoneSignupReloginAfterBindEmailEnabled: false,
autoRunSkipFailures: false, autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
signupMethod: 'email', signupMethod: 'email',
stepExecutionRangeByFlow: { stepExecutionRangeByFlow: {
@@ -94,7 +92,7 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
currentState = { currentState = {
...currentState, ...currentState,
...extraState, ...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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? currentState.autoRunCurrentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? currentState.autoRunCurrentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? currentState.autoRunTotalRuns ?? 1, 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 () => {}, ensureHotmailMailboxReadyForAutoRunRound: async () => {},
getAutoRunStatusPayload: (phase, 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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1, autoRunTotalRuns: payload.totalRuns ?? 1,
@@ -179,8 +177,6 @@ test('auto-run controller preserves kiro flow across fresh reset and starts from
phoneSignupReloginAfterBindEmailEnabled: false, phoneSignupReloginAfterBindEmailEnabled: false,
autoRunSkipFailures: false, autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
signupMethod: 'email', signupMethod: 'email',
stepExecutionRangeByFlow: { stepExecutionRangeByFlow: {
@@ -445,8 +441,6 @@ test('auto-run controller stops immediately on kiro proxy failures even when ski
flowId: 'kiro', flowId: 'kiro',
autoRunSkipFailures: true, autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0, autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
nodeStatuses: { nodeStatuses: {
'kiro-open-register-page': 'pending', '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 }); events.phases.push({ phase, ...payload });
currentState = { currentState = {
...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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? currentState.autoRunCurrentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? currentState.autoRunCurrentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? currentState.autoRunTotalRuns ?? 1, 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 () => {}, ensureHotmailMailboxReadyForAutoRunRound: async () => {},
getAutoRunStatusPayload: (phase, 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, autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0, autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1, 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 () => { test('launchAutoRunTimerPlan ignores stale timer plans after stop invalidates the session', async () => {
const api = new Function(` 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_BETWEEN_ROUNDS = 'between_rounds';
const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry'; const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry';
const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3; const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
@@ -76,14 +75,15 @@ let autoRunAttemptRun = 0;
let autoRunSessionId = 0; let autoRunSessionId = 0;
const state = { const state = {
autoRunDelayEnabled: false,
autoRunTimerPlan: { autoRunTimerPlan: {
kind: AUTO_RUN_TIMER_KIND_SCHEDULED_START, kind: AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
fireAt: Date.now() + 60_000, fireAt: Date.now() + 60_000,
totalRuns: 2, totalRuns: 2,
autoRunSkipFailures: false, autoRunSkipFailures: false,
autoRunSessionId: 42, autoRunSessionId: 42,
countdownTitle: '已计划自动运行', currentRun: 1,
attemptRun: 1,
countdownTitle: '等待继续',
countdownNote: '等待启动', countdownNote: '等待启动',
}, },
}; };
@@ -106,7 +106,6 @@ async function clearAutoRunTimerAlarm() {
async function broadcastAutoRunStatus() {} async function broadcastAutoRunStatus() {}
async function addLog() {} async function addLog() {}
async function setAutoRunDelayEnabledState() {}
function serializeAutoRunRoundSummaries(totalRuns, summaries = []) { function serializeAutoRunRoundSummaries(totalRuns, summaries = []) {
return Array.isArray(summaries) ? summaries : []; return Array.isArray(summaries) ? summaries : [];
} }
@@ -137,116 +136,10 @@ return {
const started = await api.launchAutoRunTimerPlan('alarm'); const started = await api.launchAutoRunTimerPlan('alarm');
const snapshot = api.snapshot(); 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(started, false);
assert.equal(snapshot.startCalls, 0); assert.equal(snapshot.startCalls, 0);
assert.equal(snapshot.clearStopCalls, 0); assert.equal(snapshot.clearStopCalls, 0);
assert.equal(snapshot.clearAlarmCalls, 1); assert.equal(snapshot.clearAlarmCalls, 0);
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.autoRunCurrentRun, 0); assert.equal(snapshot.autoRunCurrentRun, 0);
assert.equal(snapshot.autoRunTotalRuns, 1); assert.equal(snapshot.autoRunTotalRuns, 1);
assert.equal(snapshot.autoRunAttemptRun, 0); 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 normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : (value === 'codex2api' ? 'codex2api' : 'cpa'); }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; } function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; } 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 normalizeAutoStepDelaySeconds(value) { return value == null || value === '' ? null : Number(value); }
function normalizeMailProvider(value) { return String(value || '').trim().toLowerCase() || '163'; } function normalizeMailProvider(value) { return String(value || '').trim().toLowerCase() || '163'; }
function normalizeMail2925Mode(value) { return String(value || '').trim().toLowerCase() === 'receive' ? 'receive' : 'provide'; } 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); 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', () => { test('background auth chain set does not include Plus session import nodes', () => {
const authChainStart = source.indexOf('const AUTH_CHAIN_NODE_IDS = new Set(['); const authChainStart = source.indexOf('const AUTH_CHAIN_NODE_IDS = new Set([');
const authChainEnd = source.indexOf(']);', authChainStart); 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 api = new Function(`
const LOG_PREFIX = '[test]'; const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000; const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${OAUTH_TIMEOUT_BUDGET_TEST_REGISTRY}
${extractFunction('resolveOAuthTimeoutBudgetScope')}
${extractFunction('shouldUseOAuthTimeoutBudget')}
${extractFunction('normalizeOAuthFlowDeadlineAt')} ${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')} ${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')} ${extractFunction('getOAuthFlowRemainingMs')}
@@ -444,6 +474,8 @@ return {
step: 8, step: 8,
actionLabel: '登录验证码流程', actionLabel: '登录验证码流程',
state: { state: {
activeFlowId: 'openai',
targetId: 'cpa',
oauthUrl: 'https://oauth.example/current', oauthUrl: 'https://oauth.example/current',
oauthFlowDeadlineAt: Date.now() + 1200, oauthFlowDeadlineAt: Date.now() + 1200,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/old', 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 api = new Function(`
const LOG_PREFIX = '[test]'; const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000; const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${OAUTH_TIMEOUT_BUDGET_TEST_REGISTRY}
${extractFunction('buildOAuthFlowTimeoutError')} ${extractFunction('buildOAuthFlowTimeoutError')}
${extractFunction('resolveOAuthTimeoutBudgetScope')}
${extractFunction('shouldUseOAuthTimeoutBudget')}
${extractFunction('normalizeOAuthFlowDeadlineAt')} ${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')} ${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')} ${extractFunction('getOAuthFlowRemainingMs')}
@@ -471,6 +506,8 @@ return {
step: 8, step: 8,
actionLabel: '登录验证码流程', actionLabel: '登录验证码流程',
state: { state: {
activeFlowId: 'openai',
targetId: 'cpa',
oauthUrl: 'https://oauth.example/current', oauthUrl: 'https://oauth.example/current',
oauthFlowDeadlineAt: Date.now() + 1200, oauthFlowDeadlineAt: Date.now() + 1200,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/current', oauthFlowDeadlineSourceUrl: 'https://oauth.example/current',
@@ -481,11 +518,14 @@ return {
assert(timeoutMs >= 1000); 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 api = new Function(`
const LOG_PREFIX = '[test]'; const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000; const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${OAUTH_TIMEOUT_BUDGET_TEST_REGISTRY}
${extractFunction('buildOAuthFlowTimeoutError')} ${extractFunction('buildOAuthFlowTimeoutError')}
${extractFunction('resolveOAuthTimeoutBudgetScope')}
${extractFunction('shouldUseOAuthTimeoutBudget')}
${extractFunction('normalizeOAuthFlowDeadlineAt')} ${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')} ${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')} ${extractFunction('getOAuthFlowRemainingMs')}
@@ -499,7 +539,8 @@ return {
step: 9, step: 9,
actionLabel: 'OAuth localhost 回调', actionLabel: 'OAuth localhost 回调',
state: { state: {
oauthFlowTimeoutEnabled: false, activeFlowId: 'openai',
targetId: 'sub2api',
oauthUrl: 'https://oauth.example/current', oauthUrl: 'https://oauth.example/current',
oauthFlowDeadlineAt: Date.now() - 1000, oauthFlowDeadlineAt: Date.now() - 1000,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/current', oauthFlowDeadlineSourceUrl: 'https://oauth.example/current',
@@ -509,16 +550,18 @@ return {
assert.equal(timeoutMs, 15000); 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 = { const events = {
stateUpdates: [], stateUpdates: [],
logs: [], logs: [],
}; };
const api = new Function('events', ` const api = new Function('events', `
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000; const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${OAUTH_TIMEOUT_BUDGET_TEST_REGISTRY}
async function getState() { async function getState() {
return { return {
oauthFlowTimeoutEnabled: false, activeFlowId: 'openai',
targetId: 'sub2api',
oauthFlowDeadlineAt: Date.now() - 1000, oauthFlowDeadlineAt: Date.now() - 1000,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/old', oauthFlowDeadlineSourceUrl: 'https://oauth.example/old',
}; };
@@ -529,6 +572,8 @@ async function setState(update) {
async function addLog(message, level) { async function addLog(message, level) {
events.logs.push({ message, level }); events.logs.push({ message, level });
} }
${extractFunction('resolveOAuthTimeoutBudgetScope')}
${extractFunction('shouldUseOAuthTimeoutBudget')}
${extractFunction('normalizeOAuthFlowSourceUrl')} ${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('startOAuthFlowTimeoutWindow')} ${extractFunction('startOAuthFlowTimeoutWindow')}
return { return {
@@ -546,7 +591,7 @@ return {
oauthFlowDeadlineAt: null, oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: 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 () => { 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 resolveSignupMethod(state = {}) { return normalizeSignupMethod(state?.signupMethod); }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; } function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; } 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 normalizeAutoStepDelaySeconds(value, fallback = null) { return value == null || value === '' ? fallback : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; } function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; }
function normalizeMailProvider(value) { return String(value || '').trim().toLowerCase() || '163'; } 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 api = new Function(`
const DEFAULT_VERIFICATION_RESEND_COUNT = 4; const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PERSISTED_SETTING_DEFAULTS = { const PERSISTED_SETTING_DEFAULTS = {
panelMode: 'cpa', panelMode: 'cpa',
oauthFlowTimeoutEnabled: true,
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT, verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
mailProvider: '163', mailProvider: '163',
@@ -180,7 +178,6 @@ function normalizeSignupMethod(value = '') { return String(value || '').trim().t
function resolveSignupMethod(state = {}) { return normalizeSignupMethod(state?.signupMethod); } function resolveSignupMethod(state = {}) { return normalizeSignupMethod(state?.signupMethod); }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; } function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; } 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 normalizeAutoStepDelaySeconds(value, fallback = null) { return value == null || value === '' ? fallback : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; } function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; }
function normalizeMailProvider(value) { return String(value || '').trim().toLowerCase() || '163'; } 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({ assert.deepEqual(api.buildPersistentSettingsPayload({
oauthFlowTimeoutEnabled: false, removedUiOnlyFlag: false,
}), { }), {});
oauthFlowTimeoutEnabled: false,
});
const defaults = api.buildPersistentSettingsPayload({}, { fillDefaults: true }); 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 source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = {}; const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(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: {}, stepStatuses: {},
}), }),
normalizeRunCount: (value) => Number(value) || 1, normalizeRunCount: (value) => Number(value) || 1,
scheduleAutoRun: async () => {
calls.push({ type: 'scheduleAutoRun' });
return { ok: true };
},
setState: async (updates) => { setState: async (updates) => {
calls.push({ type: 'setState', 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 不支持手机号注册。/ /当前 flow 不支持手机号注册。/
); );
await assert.rejects(
() => router.handleMessage({
type: 'SCHEDULE_AUTO_RUN',
payload: {
totalRuns: 2,
delayMinutes: 5,
autoRunSkipFailures: false,
},
}),
/当前 flow 不支持手机号注册。/
);
assert.deepStrictEqual(calls, []); 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) { function normalizeAutoRunFallbackThreadIntervalMinutes(value) {
return Math.max(0, Math.floor(Number(value) || 0)); 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) { function normalizeAutoStepDelaySeconds(value, fallback = null) {
const numeric = Number(value); const numeric = Number(value);
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : fallback; 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/); assert.match(source, /finalizeDesktopAuthorizeCallback/);
}); });
test('kiro desktop authorize runner delegates OTP mail polling to the shared flow mail service', () => {
const source = fs.readFileSync('flows/kiro/background/desktop-authorize-runner.js', 'utf8');
assert.match(source, /pollFlowVerificationCode/);
assert.doesNotMatch(source, /buildDesktopOtpPollPayload/);
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
});
test('background wires the Kiro register injector into desktop authorization runner', () => { test('background wires the Kiro register injector into desktop authorization runner', () => {
const source = fs.readFileSync('background.js', 'utf8'); const source = fs.readFileSync('background.js', 'utf8');
const start = source.indexOf('const kiroDesktopAuthorizeRunner = self.MultiPageBackgroundKiroDesktopAuthorizeRunner?.createKiroDesktopAuthorizeRunner({'); const start = source.indexOf('const kiroDesktopAuthorizeRunner = self.MultiPageBackgroundKiroDesktopAuthorizeRunner?.createKiroDesktopAuthorizeRunner({');
@@ -37,6 +37,18 @@ test('kiro register runner uses a shared 3-minute page-load timeout budget', ()
assert.match(source, /onRetryableError: buildKiroRetryRecovery\(tabId, \{\s*\.\.\.options,\s*timeoutBudget,/); assert.match(source, /onRetryableError: buildKiroRetryRecovery\(tabId, \{\s*\.\.\.options,\s*timeoutBudget,/);
}); });
test('kiro register runner delegates verification mail polling to the shared flow mail service', () => {
const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8');
assert.match(source, /pollFlowVerificationCode/);
assert.doesNotMatch(source, /buildKiroVerificationPollPayload/);
assert.doesNotMatch(source, /pollHotmailVerificationCode/);
assert.doesNotMatch(source, /pollLuckmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudflareTempEmailVerificationCode/);
assert.doesNotMatch(source, /pollCloudMailVerificationCode/);
assert.doesNotMatch(source, /pollYydsMailVerificationCode/);
assert.doesNotMatch(source, /sendToMailContentScriptResilient/);
});
test('kiro register consent step treats Kiro Web signed-in page as completion', () => { test('kiro register consent step treats Kiro Web signed-in page as completion', () => {
const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8'); const source = fs.readFileSync('flows/kiro/background/register-runner.js', 'utf8');
assert.match(source, /readKiroRegisterPageState\(tabId, \{/); assert.match(source, /readKiroRegisterPageState\(tabId, \{/);
@@ -231,8 +243,8 @@ test('kiro verification polling uses the registration email field instead of pag
getState: async () => currentState, getState: async () => currentState,
getTabId: async () => 103, getTabId: async () => 103,
isTabAlive: async () => true, isTabAlive: async () => true,
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => { pollFlowVerificationCode: async (options) => {
pollPayloads.push(payload); pollPayloads.push(options);
return { code: '123456', emailTimestamp: 2000, mailId: 'mail-1' }; return { code: '123456', emailTimestamp: 2000, mailId: 'mail-1' };
}, },
sendToContentScriptResilient: async (_sourceId, message) => { 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.length, 1);
assert.equal(pollPayloads[0].targetEmail, 'skater-twine-carve@duck.com'); assert.equal(pollPayloads[0].flowId, 'kiro');
assert.deepEqual(pollPayloads[0].targetEmailHints, ['skater-twine-carve@duck.com']); 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) => ( assert.equal(sentMessages.some((message) => (
message.type === 'EXECUTE_NODE' message.type === 'EXECUTE_NODE'
&& message.nodeId === 'kiro-submit-verification-code' && 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, getState: async () => currentState,
getTabId: async () => null, getTabId: async () => null,
isTabAlive: async () => false, isTabAlive: async () => false,
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => { pollFlowVerificationCode: async (options) => {
pollPayloads.push(payload); pollPayloads.push(options);
return { code: '248680', emailTimestamp: 2000, mailId: 'mail-active' }; return { code: '248680', emailTimestamp: 2000, mailId: 'mail-active' };
}, },
registerTab: async (source, tabId) => { 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.deepEqual(registeredTabs, [{ source: 'kiro-register-page', tabId: 301 }]);
assert.equal(getKiroRuntime(statePatches[0]).session?.registerTabId, 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) => ( assert.equal(sentMessages.some((message) => (
message.type === 'EXECUTE_NODE' message.type === 'EXECUTE_NODE'
&& message.nodeId === 'kiro-submit-verification-code' && 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', 'content/utils.js',
'flows/kiro/content/register-page.js', 'flows/kiro/content/register-page.js',
], ],
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => { pollFlowVerificationCode: async (options) => {
pollPayloads.push(payload); pollPayloads.push(options);
return { code: '248680', emailTimestamp: 2000, mailId: 'mail-reinject' }; return { code: '248680', emailTimestamp: 2000, mailId: 'mail-reinject' };
}, },
sendToContentScriptResilient: async (_sourceId, message) => { sendToContentScriptResilient: async (_sourceId, message) => {
@@ -464,7 +482,10 @@ test('kiro verification step reinjects the register driver when only the generic
'content/utils.js', 'content/utils.js',
'flows/kiro/content/register-page.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'); 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 assert = require('node:assert/strict');
const fs = require('node:fs'); const fs = require('node:fs');
test('background imports mail rule registry and OpenAI mail rules modules', () => { test('background imports mail rule registry and flow mail rules modules', () => {
const source = fs.readFileSync('background.js', 'utf8'); const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /background\/mail-rule-registry\.js/); assert.match(source, /background\/mail-rule-registry\.js/);
assert.match(source, /flows\/openai\/mail-rules\.js/); assert.match(source, /flows\/openai\/mail-rules\.js/);
assert.match(source, /flows\/kiro\/mail-rules\.js/);
assert.match(source, /flows\/grok\/mail-rules\.js/);
assert.match(source, /background\/flow-mail-polling\.js/);
}); });
test('mail rule registry exposes canonical OpenAI verification poll payloads', () => { test('mail rule registry exposes canonical OpenAI verification poll payloads', () => {
@@ -133,3 +136,185 @@ test('mail rule registry rejects unknown active flow ids instead of silently usi
/未找到 flow=site-a 的邮件轮询规则构造器/ /未找到 flow=site-a 的邮件轮询规则构造器/
); );
}); });
test('mail rule registry exposes Kiro AWS verification poll payloads by node', () => {
const stateSource = fs.readFileSync('flows/kiro/background/state.js', 'utf8');
const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
const kiroSource = fs.readFileSync('flows/kiro/mail-rules.js', 'utf8');
const globalScope = {};
new Function('self', `${stateSource}; ${registrySource}; ${kiroSource}; return self;`)(globalScope);
const kiroMailRules = globalScope.MultiPageKiroMailRules.createKiroMailRules({
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 16000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 17,
});
const registry = globalScope.MultiPageBackgroundMailRuleRegistry.createMailRuleRegistry({
defaultFlowId: 'openai',
flowBuilders: {
kiro: kiroMailRules,
},
});
const baseState = {
activeFlowId: 'kiro',
mailProvider: '2925',
mail2925Mode: 'receive',
runtimeState: {
flowState: {
kiro: {
register: {
email: 'kiro-user@example.com',
},
},
},
},
};
assert.deepEqual(
registry.buildVerificationPollPayloadForNode(
'kiro-submit-verification-code',
baseState,
{ filterAfterTimestamp: 12345 }
),
{
flowId: 'kiro',
ruleId: 'kiro-submit-verification-code',
nodeId: 'kiro-submit-verification-code',
step: 4,
artifactType: 'code',
codePatterns: [
{
source: '(?:verification\\s*code|验证码|Your code is|code is)[:\\s]*(\\d{6})',
flags: 'gi',
},
{
source: '^\\s*(\\d{6})\\s*$',
flags: 'gm',
},
{
source: '>\\s*(\\d{6})\\s*<',
flags: 'g',
},
],
filterAfterTimestamp: 12345,
requiredKeywords: ['verification', '验证码', 'code', 'aws'],
senderFilters: [
'no-reply@signin.aws',
'no-reply@login.awsapps.com',
'noreply@amazon.com',
'account-update@amazon.com',
'no-reply@aws.amazon.com',
'noreply@aws.amazon.com',
'aws',
],
subjectFilters: ['aws builder id', 'verification', '验证码', 'code', 'aws'],
targetEmail: 'kiro-user@example.com',
targetEmailHints: ['kiro-user@example.com'],
mail2925MatchTargetEmail: true,
maxAttempts: 17,
intervalMs: 16000,
}
);
const desktopPayload = registry.buildVerificationPollPayloadForNode(
'kiro-complete-desktop-authorize',
{
...baseState,
mailProvider: 'luckmail-api',
mail2925Mode: 'provide',
}
);
assert.equal(desktopPayload.ruleId, 'kiro-complete-desktop-authorize');
assert.equal(desktopPayload.step, 8);
assert.equal(desktopPayload.mail2925MatchTargetEmail, false);
assert.equal(desktopPayload.maxAttempts, 3);
assert.equal(desktopPayload.intervalMs, 15000);
});
test('mail rule registry exposes Grok xAI verification poll payloads by node', () => {
const stateSource = fs.readFileSync('flows/grok/background/state.js', 'utf8');
const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
const grokSource = fs.readFileSync('flows/grok/mail-rules.js', 'utf8');
const globalScope = {};
new Function('self', `${stateSource}; ${registrySource}; ${grokSource}; return self;`)(globalScope);
const grokMailRules = globalScope.MultiPageGrokMailRules.createGrokMailRules({
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 16000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 17,
});
const registry = globalScope.MultiPageBackgroundMailRuleRegistry.createMailRuleRegistry({
defaultFlowId: 'openai',
flowBuilders: {
grok: grokMailRules,
},
});
assert.deepEqual(
registry.buildVerificationPollPayloadForNode(
'grok-submit-verification-code',
{
activeFlowId: 'grok',
mailProvider: '2925',
mail2925Mode: 'receive',
runtimeState: {
flowState: {
grok: {
register: {
email: 'grok-user@example.com',
},
},
},
},
},
{ filterAfterTimestamp: 12345 }
),
{
flowId: 'grok',
ruleId: 'grok-submit-verification-code',
nodeId: 'grok-submit-verification-code',
step: 3,
artifactType: 'code',
codePatterns: [
{
source: '\\b([A-Z0-9]{3}-[A-Z0-9]{3})\\b',
flags: 'gi',
},
{
source: '(?:verification\\s*code|confirmation\\s*code|code\\s*is)[:\\s]*(\\d{6})',
flags: 'gi',
},
{
source: '(?:验证码|代码|确认码)[:\\s为]+(\\d{6})',
flags: 'gi',
},
{
source: '(?<!#)\\b(\\d{6})\\b',
flags: 'g',
},
],
filterAfterTimestamp: 12345,
requiredKeywords: ['xai', 'x.ai', 'grok', 'verification', 'confirmation', 'code', '验证码', '确认码'],
senderFilters: ['x.ai', 'xai', 'grok'],
subjectFilters: ['xai', 'x.ai', 'grok', 'verification', 'confirmation', 'code', '验证码', '确认码'],
targetEmail: 'grok-user@example.com',
targetEmailHints: ['grok-user@example.com'],
mail2925MatchTargetEmail: true,
maxAttempts: 17,
intervalMs: 16000,
}
);
const luckmailPayload = registry.buildVerificationPollPayloadForNode(
'grok-submit-verification-code',
{
activeFlowId: 'grok',
mailProvider: 'luckmail-api',
mail2925Mode: 'provide',
grokEmail: 'fallback@example.com',
}
);
assert.equal(luckmailPayload.mail2925MatchTargetEmail, false);
assert.equal(luckmailPayload.maxAttempts, 3);
assert.equal(luckmailPayload.intervalMs, 15000);
});
@@ -625,6 +625,47 @@ test('SAVE_SETTING mirrors activeFlowId into flowId when switching to kiro flow'
}); });
}); });
test('CLEAR_GROK_SSO_COOKIES delegates canonical runtime cleanup to background', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console };
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
let called = false;
const router = api.createMessageRouter({
clearGrokSsoCookies: async () => {
called = true;
return {
ok: true,
state: {
grokSsoCookie: '',
grokSsoCookies: [],
runtimeState: {
flowState: {
grok: {
sso: {
currentCookie: '',
cookies: [],
extractedAt: 0,
},
},
},
},
},
};
},
});
const response = await router.handleMessage({
type: 'CLEAR_GROK_SSO_COOKIES',
source: 'sidepanel',
payload: {},
});
assert.equal(called, true);
assert.equal(response.ok, true);
assert.deepStrictEqual(response.state.grokSsoCookies, []);
assert.equal(response.state.runtimeState.flowState.grok.sso.currentCookie, '');
});
test('SAVE_SETTING syncs canonical kiro settingsState back into session state', async () => { test('SAVE_SETTING syncs canonical kiro settingsState back into session state', async () => {
const source = fs.readFileSync('background/message-router.js', 'utf8'); const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = { console }; const globalScope = { console };
@@ -35,7 +35,6 @@ function createRouterWithFinalNode(options = {}) {
buildLuckmailSessionSettingsPayload: () => ({}), buildLuckmailSessionSettingsPayload: () => ({}),
buildPersistentSettingsPayload: () => ({}), buildPersistentSettingsPayload: () => ({}),
broadcastDataUpdate: () => {}, broadcastDataUpdate: () => {},
cancelScheduledAutoRun: async () => {},
checkIcloudSession: async () => {}, checkIcloudSession: async () => {},
clearAutoRunTimerAlarm: async () => {}, clearAutoRunTimerAlarm: async () => {},
clearLuckmailRuntimeState: async () => {}, clearLuckmailRuntimeState: async () => {},
@@ -87,7 +86,6 @@ function createRouterWithFinalNode(options = {}) {
listLuckmailPurchasesForManagement: async () => [], listLuckmailPurchasesForManagement: async () => [],
normalizeHotmailAccounts: (items) => items, normalizeHotmailAccounts: (items) => items,
normalizeRunCount: (value) => value, normalizeRunCount: (value) => value,
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
notifyNodeComplete: () => {}, notifyNodeComplete: () => {},
notifyNodeError: () => {}, notifyNodeError: () => {},
patchHotmailAccount: async () => {}, patchHotmailAccount: async () => {},
@@ -96,7 +94,6 @@ function createRouterWithFinalNode(options = {}) {
requestStop: async () => {}, requestStop: async () => {},
resetState: async () => {}, resetState: async () => {},
resumeAutoRun: async () => {}, resumeAutoRun: async () => {},
scheduleAutoRun: async () => {},
selectLuckmailPurchase: async () => {}, selectLuckmailPurchase: async () => {},
setCurrentHotmailAccount: async () => {}, setCurrentHotmailAccount: async () => {},
setCurrentMail2925Account: async () => {}, setCurrentMail2925Account: async () => {},
@@ -103,7 +103,6 @@ function createRouter(overrides = {}) {
broadcastDataUpdate: (updates) => { broadcastDataUpdate: (updates) => {
events.broadcasts.push(updates); events.broadcasts.push(updates);
}, },
cancelScheduledAutoRun: async () => {},
checkIcloudSession: async () => {}, checkIcloudSession: async () => {},
clearAutoRunTimerAlarm: async () => {}, clearAutoRunTimerAlarm: async () => {},
clearLuckmailRuntimeState: async () => {}, clearLuckmailRuntimeState: async () => {},
@@ -171,7 +170,6 @@ function createRouter(overrides = {}) {
listLuckmailPurchasesForManagement: async () => [], listLuckmailPurchasesForManagement: async () => [],
normalizeHotmailAccounts: (items) => items, normalizeHotmailAccounts: (items) => items,
normalizeRunCount: (value) => value, normalizeRunCount: (value) => value,
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
notifyNodeComplete: (nodeId, payload) => { notifyNodeComplete: (nodeId, payload) => {
events.notifyCompletions.push({ step: getStepForNode(nodeId), nodeId, payload }); events.notifyCompletions.push({ step: getStepForNode(nodeId), nodeId, payload });
}, },
@@ -183,7 +181,6 @@ function createRouter(overrides = {}) {
requestStop: async () => {}, requestStop: async () => {},
resetState: async () => {}, resetState: async () => {},
resumeAutoRun: async () => {}, resumeAutoRun: async () => {},
scheduleAutoRun: async () => {},
selectLuckmailPurchase: async () => {}, selectLuckmailPurchase: async () => {},
setCurrentHotmailAccount: async () => {}, setCurrentHotmailAccount: async () => {},
setEmailState: async (email) => { setEmailState: async (email) => {
@@ -188,6 +188,7 @@ return {
plusPaymentMethod: 'gopay', plusPaymentMethod: 'gopay',
plusAccountAccessStrategy: 'oauth', plusAccountAccessStrategy: 'oauth',
signupMethod: 'phone', signupMethod: 'phone',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false, phoneSignupReloginAfterBindEmailEnabled: false,
}]); }]);
assert.equal(steps[0].title, '注册并输入手机号'); 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-client\.js/);
assert.match(source, /flows\/kiro\/background\/desktop-authorize-runner\.js/); assert.match(source, /flows\/kiro\/background\/desktop-authorize-runner\.js/);
assert.match(source, /flows\/kiro\/background\/publisher-kiro-rs\.js/); assert.match(source, /flows\/kiro\/background\/publisher-kiro-rs\.js/);
assert.match(source, /flows\/grok\/background\/state\.js/);
assert.match(source, /flows\/grok\/background\/register-runner\.js/);
assert.match(source, /flows\/grok\/background\/publisher-webchat2api\.js/);
assert.doesNotMatch(source, /background\/steps\/kiro-device-auth\.js/); assert.doesNotMatch(source, /background\/steps\/kiro-device-auth\.js/);
assert.match(source, /const kiroRegisterRunner = self\.MultiPageBackgroundKiroRegisterRunner\?\.createKiroRegisterRunner\(/); assert.match(source, /const kiroRegisterRunner = self\.MultiPageBackgroundKiroRegisterRunner\?\.createKiroRegisterRunner\(/);
assert.match(source, /const kiroDesktopAuthorizeRunner = self\.MultiPageBackgroundKiroDesktopAuthorizeRunner\?\.createKiroDesktopAuthorizeRunner\(/); assert.match(source, /const kiroDesktopAuthorizeRunner = self\.MultiPageBackgroundKiroDesktopAuthorizeRunner\?\.createKiroDesktopAuthorizeRunner\(/);
assert.match(source, /const kiroPublisher = self\.MultiPageBackgroundKiroPublisherKiroRs\?\.createKiroRsPublisher\(/); assert.match(source, /const kiroPublisher = self\.MultiPageBackgroundKiroPublisherKiroRs\?\.createKiroRsPublisher\(/);
assert.match(source, /const grokRegisterRunner = self\.MultiPageBackgroundGrokRegisterRunner\?\.createGrokRegisterRunner\(/);
assert.match(source, /const grokWebchat2ApiPublisher = self\.MultiPageBackgroundGrokPublisherWebchat2Api\?\.createGrokWebchat2ApiPublisher\(/);
assert.match(source, /'kiro-open-register-page': \(state\) => kiroRegisterRunner\.executeKiroOpenRegisterPage\(state\)/); assert.match(source, /'kiro-open-register-page': \(state\) => kiroRegisterRunner\.executeKiroOpenRegisterPage\(state\)/);
assert.match(source, /'kiro-submit-email': \(state\) => kiroRegisterRunner\.executeKiroSubmitEmail\(state\)/); assert.match(source, /'kiro-submit-email': \(state\) => kiroRegisterRunner\.executeKiroSubmitEmail\(state\)/);
@@ -32,11 +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-start-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroStartDesktopAuthorize\(state\)/);
assert.match(source, /'kiro-complete-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroCompleteDesktopAuthorize\(state\)/); assert.match(source, /'kiro-complete-desktop-authorize': \(state\) => kiroDesktopAuthorizeRunner\.executeKiroCompleteDesktopAuthorize\(state\)/);
assert.match(source, /'kiro-upload-credential': \(state\) => kiroPublisher\.executeKiroUploadCredential\(state\)/); assert.match(source, /'kiro-upload-credential': \(state\) => kiroPublisher\.executeKiroUploadCredential\(state\)/);
assert.match(source, /'grok-open-signup-page': \(state\) => grokRegisterRunner\.executeGrokOpenSignupPage\(state\)/);
assert.match(source, /'grok-submit-email': \(state\) => grokRegisterRunner\.executeGrokSubmitEmail\(state\)/);
assert.match(source, /'grok-submit-verification-code': \(state\) => grokRegisterRunner\.executeGrokSubmitVerificationCode\(state\)/);
assert.match(source, /'grok-submit-profile': \(state\) => grokRegisterRunner\.executeGrokSubmitProfile\(state\)/);
assert.match(source, /'grok-extract-sso-cookie': \(state\) => grokRegisterRunner\.executeGrokExtractSsoCookie\(state\)/);
assert.match(source, /'grok-upload-sso-to-webchat2api': \(state\) => grokWebchat2ApiPublisher\.executeGrokUploadSsoToWebchat2Api\(state\)/);
assert.match( assert.match(
source, source,
/'kiro-open-register-page',[\s\S]*'kiro-submit-email',[\s\S]*'kiro-submit-name',[\s\S]*'kiro-submit-verification-code',[\s\S]*'kiro-submit-password',[\s\S]*'kiro-complete-register-consent',[\s\S]*'kiro-start-desktop-authorize',[\s\S]*'kiro-complete-desktop-authorize',[\s\S]*'kiro-upload-credential'/ /'kiro-open-register-page',[\s\S]*'kiro-submit-email',[\s\S]*'kiro-submit-name',[\s\S]*'kiro-submit-verification-code',[\s\S]*'kiro-submit-password',[\s\S]*'kiro-complete-register-consent',[\s\S]*'kiro-start-desktop-authorize',[\s\S]*'kiro-complete-desktop-authorize',[\s\S]*'kiro-upload-credential'/
); );
assert.match(
source,
/'grok-open-signup-page',[\s\S]*'grok-submit-email',[\s\S]*'grok-submit-verification-code',[\s\S]*'grok-submit-profile',[\s\S]*'grok-extract-sso-cookie',[\s\S]*'grok-upload-sso-to-webchat2api'/
);
}); });
test('GoPay approve executor receives debugger click and manual OTP helpers', () => { test('GoPay approve executor receives debugger click and manual OTP helpers', () => {
@@ -201,3 +201,59 @@ return {
true 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 step8PendingReject = null;
let resumeWaiter = null; let resumeWaiter = null;
const STOP_ERROR_MESSAGE = '流程已被用户停止。'; const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
const DEFAULT_STATE = { const DEFAULT_STATE = {
stepStatuses: Object.fromEntries([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((step) => [step, 'pending'])), 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() { function getPendingAutoRunTimerPlan() {
return null; return null;
} }
async function cancelScheduledAutoRun() {}
async function clearAutoRunTimerAlarm() {} async function clearAutoRunTimerAlarm() {}
function clearStopRequest() { function clearStopRequest() {
stopRequested = false; 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', () => { test('contribution registry fails fast when a published flow has no adapter', () => {
const api = loadApi(); const api = loadApi();
assert.deepEqual(api.getPublishedContributionFlowIds(), ['openai', 'kiro']);
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(), true);
assert.equal(api.assertPublishedFlowsHaveContributionAdapters(['openai', 'kiro']), true); assert.equal(api.assertPublishedFlowsHaveContributionAdapters(['openai', 'kiro']), true);
assert.throws( assert.throws(
() => api.assertPublishedFlowsHaveContributionAdapters(['openai', 'missing-flow']), () => api.assertPublishedFlowsHaveContributionAdapters(['openai', 'missing-flow']),
+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.canUsePhoneSignup, true);
assert.equal(enabledState.effectiveSignupMethod, 'phone'); assert.equal(enabledState.effectiveSignupMethod, 'phone');
assert.equal(enabledState.shouldWarnCpaPhoneSignup, true); assert.equal(enabledState.shouldWarnCpaPhoneSignup, true);
assert.equal(enabledState.targetCapabilities.usesOauthTimeoutBudget, true);
assert.equal(enabledState.stepDefinitionOptions.phoneVerificationEnabled, true);
assert.deepEqual(enabledState.effectiveSignupMethods, ['email', 'phone']); assert.deepEqual(enabledState.effectiveSignupMethods, ['email', 'phone']);
const plusLockedState = registry.resolveSidepanelCapabilities({ 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.canUsePhoneSignup, false);
assert.equal(plusLockedState.effectiveSignupMethod, 'email'); assert.equal(plusLockedState.effectiveSignupMethod, 'email');
assert.equal(plusLockedState.stepDefinitionOptions.phoneVerificationEnabled, true);
assert.equal(plusLockedState.shouldWarnCpaPhoneSignup, false); assert.equal(plusLockedState.shouldWarnCpaPhoneSignup, false);
assert.equal(plusLockedState.targetCapabilities.usesOauthTimeoutBudget, false);
assert.deepEqual(plusLockedState.effectiveSignupMethods, ['email']); 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.flowCapabilities.contributionAdapterIds, ['kiro-builder-id']);
assert.deepEqual( assert.deepEqual(
capabilityState.visibleGroupIds, 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', () => { test('flow registry exposes canonical flow and target metadata', () => {
const { flowRegistry } = loadApis(); const { flowRegistry } = loadApis();
assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro']); assert.deepEqual(flowRegistry.getRegisteredFlowIds(), ['openai', 'kiro', 'grok']);
assert.equal(flowRegistry.normalizeFlowId('kiro'), 'kiro'); assert.equal(flowRegistry.normalizeFlowId('kiro'), 'kiro');
assert.equal(flowRegistry.normalizeFlowId('grok'), 'grok');
assert.equal(flowRegistry.normalizeFlowId('unknown'), 'openai'); assert.equal(flowRegistry.normalizeFlowId('unknown'), 'openai');
assert.equal(flowRegistry.getFlowLabel('openai'), 'Codex / OpenAI'); assert.equal(flowRegistry.getFlowLabel('openai'), 'Codex / OpenAI');
assert.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('openai', 'sub2api'), 'sub2api');
assert.equal(flowRegistry.normalizeTargetId('kiro', 'anything-else'), 'kiro-rs'); assert.equal(flowRegistry.normalizeTargetId('kiro', 'anything-else'), 'kiro-rs');
assert.equal(flowRegistry.normalizeTargetId('grok', 'anything-else'), 'webchat2api');
assert.deepEqual( assert.deepEqual(
flowRegistry.getVisibleGroupIds('openai', 'cpa'), flowRegistry.getVisibleGroupIds('openai', 'cpa'),
['openai-plus', 'openai-phone', 'openai-oauth', 'openai-step6', 'openai-target-cpa', 'service-account', 'service-email', 'service-proxy'] ['openai-plus', 'openai-phone', 'shared-auto-run', 'openai-oauth', 'openai-step6', 'shared-settings-actions', 'openai-target-cpa', 'service-account', 'service-email', 'service-proxy']
); );
assert.deepEqual( assert.deepEqual(
flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'), flowRegistry.getVisibleGroupIds('kiro', 'kiro-rs'),
['kiro-runtime-status', 'kiro-target-kiro-rs', 'service-account', 'service-email', 'service-proxy'] ['kiro-runtime-status', '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( assert.deepEqual(
flowRegistry.getTargetOptions('openai').map((entry) => entry.id), flowRegistry.getTargetOptions('openai').map((entry) => entry.id),
['cpa', 'sub2api', 'codex2api'] ['cpa', 'sub2api', 'codex2api']
); );
assert.deepEqual(
flowRegistry.getTargetOptions('grok').map((entry) => entry.id),
['webchat2api']
);
assert.deepEqual( assert.deepEqual(
flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds, flowRegistry.getSettingsGroupDefinition('openai-plus')?.rowIds,
['row-plus-mode', 'row-plus-account-access-strategy', 'row-plus-payment-method'] ['row-plus-mode', 'row-plus-account-access-strategy', 'row-plus-payment-method']
); );
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.getPublicationTargetDefinition('kiro', 'kiro-rs')?.label, 'kiro.rs');
assert.equal(flowRegistry.getFlowCapabilities('openai').supportsAccountContribution, true); assert.equal(flowRegistry.getFlowCapabilities('openai').supportsAccountContribution, true);
assert.equal(flowRegistry.getFlowCapabilities('kiro').supportsAccountContribution, true); assert.equal(flowRegistry.getFlowCapabilities('kiro').supportsAccountContribution, true);
assert.equal(flowRegistry.getFlowCapabilities('grok').supportsAccountContribution, false);
assert.deepEqual(flowRegistry.getFlowCapabilities('grok').supportedTargetIds, ['webchat2api']);
assert.deepEqual( assert.deepEqual(
flowRegistry.getFlowCapabilities('openai').contributionAdapterIds, flowRegistry.getFlowCapabilities('openai').contributionAdapterIds,
['openai-oauth', 'openai-codex-file', 'openai-sub2api-file'] ['openai-oauth', 'openai-codex-file', 'openai-sub2api-file']
@@ -49,6 +89,7 @@ test('flow registry exposes canonical flow and target metadata', () => {
flowRegistry.getFlowCapabilities('kiro').contributionAdapterIds, flowRegistry.getFlowCapabilities('kiro').contributionAdapterIds,
['kiro-builder-id'] ['kiro-builder-id']
); );
assert.deepEqual(flowRegistry.getFlowCapabilities('grok').contributionAdapterIds, []);
}); });
test('settings schema normalizes view input into canonical nested namespaces', () => { test('settings schema normalizes view input into canonical nested namespaces', () => {
@@ -68,6 +109,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
stepExecutionRangeByFlow: { stepExecutionRangeByFlow: {
openai: { enabled: true, fromStep: 2, toStep: 9 }, openai: { enabled: true, fromStep: 2, toStep: 9 },
kiro: { enabled: true, fromStep: 1, toStep: 9 }, kiro: { enabled: true, fromStep: 1, toStep: 9 },
grok: { enabled: true, fromStep: 2, toStep: 4 },
}, },
}); });
@@ -78,6 +120,7 @@ test('settings schema normalizes view input into canonical nested namespaces', (
assert.equal(normalized.flows.openai.selectedTargetId, 'cpa'); assert.equal(normalized.flows.openai.selectedTargetId, 'cpa');
assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session'); assert.equal(normalized.flows.openai.plus.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(normalized.flows.kiro.selectedTargetId, 'kiro-rs'); assert.equal(normalized.flows.kiro.selectedTargetId, 'kiro-rs');
assert.equal(normalized.flows.grok.selectedTargetId, 'webchat2api');
assert.equal(normalized.flows.kiro.targets['kiro-rs'].baseUrl, 'https://kiro.example.com/admin'); assert.equal(normalized.flows.kiro.targets['kiro-rs'].baseUrl, 'https://kiro.example.com/admin');
assert.equal(normalized.flows.kiro.targets['kiro-rs'].apiKey, 'secret-key'); assert.equal(normalized.flows.kiro.targets['kiro-rs'].apiKey, 'secret-key');
assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, { assert.deepEqual(normalized.flows.kiro.autoRun.stepExecutionRange, {
@@ -85,6 +128,11 @@ test('settings schema normalizes view input into canonical nested namespaces', (
fromStep: 1, fromStep: 1,
toStep: 9, toStep: 9,
}); });
assert.deepEqual(normalized.flows.grok.autoRun.stepExecutionRange, {
enabled: true,
fromStep: 2,
toStep: 4,
});
}); });
test('settings schema lets explicit flat step range override stale canonical range', () => { test('settings schema lets explicit flat step range override stale canonical range', () => {
@@ -130,6 +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.plusAccountAccessStrategy, 'sub2api_codex_session');
assert.equal(view.settingsSchemaVersion, 5); assert.equal(view.settingsSchemaVersion, 5);
assert.equal(view.settingsState.activeFlowId, 'kiro'); assert.equal(view.settingsState.activeFlowId, 'kiro');
assert.deepEqual(view.stepExecutionRangeByFlow.grok, {
enabled: false,
fromStep: 1,
toStep: 6,
});
}); });
test('settings schema preserves CPA session strategy in canonical state and read view', () => { test('settings schema preserves CPA session strategy in canonical state and read view', () => {
@@ -143,3 +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(normalized.flows.openai.plus.plusAccountAccessStrategy, 'cpa_codex_session');
assert.equal(view.plusAccountAccessStrategy, 'cpa_codex_session'); assert.equal(view.plusAccountAccessStrategy, 'cpa_codex_session');
}); });
test('settings schema preserves registered custom flow settings without openai/kiro hardcoding', () => {
const { settingsSchema } = loadApis();
const customFlowRegistry = {
DEFAULT_FLOW_ID: 'openai',
getRegisteredFlowIds: () => ['openai', 'kiro', 'sample'],
getDefaultTargetId(flowId) {
return flowId === 'sample' ? 'sample-target' : (flowId === 'kiro' ? 'kiro-rs' : 'cpa');
},
getFlowDefinition(flowId) {
if (flowId !== 'sample') {
return null;
}
return {
id: 'sample',
defaultTargetId: 'sample-target',
settingsDefaults: {
targets: {
'sample-target': {
endpoint: 'https://sample.example.com',
},
},
autoRun: {
stepExecutionRange: { enabled: false, fromStep: 1, toStep: 3 },
},
},
};
},
getTargetDefinitions(flowId) {
if (flowId === 'sample') {
return {
'sample-target': { id: 'sample-target', label: 'Sample Target' },
};
}
if (flowId === 'kiro') {
return {
'kiro-rs': { id: 'kiro-rs', label: 'kiro.rs' },
};
}
return {
cpa: { id: 'cpa', label: 'CPA' },
sub2api: { id: 'sub2api', label: 'SUB2API' },
codex2api: { id: 'codex2api', label: 'Codex2API' },
};
},
normalizeFlowId(value = '', fallback = 'openai') {
const normalized = String(value || '').trim().toLowerCase();
return ['openai', 'kiro', 'sample'].includes(normalized)
? normalized
: (['openai', 'kiro', 'sample'].includes(fallback) ? fallback : 'openai');
},
normalizeTargetId(flowId, targetId = '', fallback = '') {
const targets = Object.keys(customFlowRegistry.getTargetDefinitions(flowId));
const normalized = String(targetId || '').trim().toLowerCase();
if (targets.includes(normalized)) {
return normalized;
}
if (targets.includes(fallback)) {
return fallback;
}
return customFlowRegistry.getDefaultTargetId(flowId);
},
};
const schema = settingsSchema.createSettingsSchema({ flowRegistry: customFlowRegistry });
const normalized = schema.normalizeSettingsState({
activeFlowId: 'sample',
targetId: 'sample-target',
settingsState: {
flows: {
sample: {
selectedTargetId: 'sample-target',
targets: {
'sample-target': {
endpoint: 'https://custom.example.com',
},
},
autoRun: {
stepExecutionRange: { enabled: true, fromStep: 2, toStep: 3 },
},
},
},
},
});
const view = schema.buildSettingsView(normalized);
assert.equal(normalized.activeFlowId, 'sample');
assert.equal(normalized.flows.sample.selectedTargetId, 'sample-target');
assert.equal(normalized.flows.sample.targets['sample-target'].endpoint, 'https://custom.example.com');
assert.deepEqual(view.stepExecutionRangeByFlow.sample, {
enabled: true,
fromStep: 2,
toStep: 3,
});
assert.equal(schema.getSelectedTargetId(normalized, 'sample'), 'sample-target');
});
+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([ const FLOW_DEFINITION_FILES = Object.freeze([
'flows/openai/index.js', 'flows/openai/index.js',
'flows/kiro/index.js', 'flows/kiro/index.js',
'flows/grok/index.js',
'flows/index.js', 'flows/index.js',
]); ]);
const FLOW_WORKFLOW_FILES = Object.freeze([ const FLOW_WORKFLOW_FILES = Object.freeze([
'flows/openai/workflow.js', 'flows/openai/workflow.js',
'flows/kiro/workflow.js', 'flows/kiro/workflow.js',
'flows/grok/workflow.js',
]); ]);
function readBundle(files = []) { function readBundle(files = []) {
@@ -263,6 +263,7 @@ const window = {
}, },
}; };
let latestState = { activeFlowId: 'openai' }; let latestState = { activeFlowId: 'openai' };
const inputPhoneVerificationEnabled = { checked: false };
let currentPlusModeEnabled = false; let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal'; let currentPlusPaymentMethod = 'paypal';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth'; 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'; const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
let currentPlusAccountAccessStrategy = 'oauth'; let currentPlusAccountAccessStrategy = 'oauth';
let currentSignupMethod = 'email'; let currentSignupMethod = 'email';
let currentPhoneVerificationEnabled = false;
let currentPhoneSignupReloginAfterBindEmailEnabled = false; let currentPhoneSignupReloginAfterBindEmailEnabled = false;
let currentStepDefinitionFlowId = 'openai'; let currentStepDefinitionFlowId = 'openai';
const DEFAULT_ACTIVE_FLOW_ID = 'openai'; const DEFAULT_ACTIVE_FLOW_ID = 'openai';
@@ -326,6 +328,7 @@ return {
plusPaymentMethod: 'paypal', plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session', plusAccountAccessStrategy: 'sub2api_codex_session',
signupMethod: 'email', signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false, phoneSignupReloginAfterBindEmailEnabled: false,
accountContributionEnabled: false, accountContributionEnabled: false,
}, },
@@ -338,6 +341,7 @@ return {
plusPaymentMethod: 'paypal', plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session', plusAccountAccessStrategy: 'sub2api_codex_session',
signupMethod: 'email', signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false, phoneSignupReloginAfterBindEmailEnabled: false,
accountContributionEnabled: 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, 'syncAutoRunState'),
extractFunction(source, 'isAutoRunLockedPhase'), extractFunction(source, 'isAutoRunLockedPhase'),
extractFunction(source, 'isAutoRunPausedPhase'), extractFunction(source, 'isAutoRunPausedPhase'),
extractFunction(source, 'isAutoRunScheduledPhase'),
extractFunction(source, 'applyAutoRunStatus'), extractFunction(source, 'applyAutoRunStatus'),
].join('\n'); ].join('\n');
@@ -96,7 +95,6 @@ let currentAutoRun = {
currentRun: 0, currentRun: 0,
totalRuns: 1, totalRuns: 1,
attemptRun: 0, attemptRun: 0,
scheduledAt: null,
countdownAt: null, countdownAt: null,
countdownTitle: '', countdownTitle: '',
countdownNote: '', countdownNote: '',
@@ -116,9 +114,8 @@ function getLockedRunCountFromEmailPool() { return lockedRunCount; }
function isCustomMailProvider() { return false; } function isCustomMailProvider() { return false; }
function usesCustomEmailPoolGenerator() { return false; } function usesCustomEmailPoolGenerator() { return false; }
function setDefaultAutoRunButton() {} function setDefaultAutoRunButton() {}
function updateAutoDelayInputState() {}
function updateFallbackThreadIntervalInputState() {} function updateFallbackThreadIntervalInputState() {}
function syncScheduledCountdownTicker() {} function syncAutoRunCountdownTicker() {}
function updateStopButtonState() {} function updateStopButtonState() {}
function getStepStatuses() { return {}; } function getStepStatuses() { return {}; }
function updateConfigMenuControls() {} function updateConfigMenuControls() {}
@@ -171,7 +168,6 @@ test('sidepanel pending auto-run start ignores stale active run count sync', ()
extractFunction(source, 'syncAutoRunState'), extractFunction(source, 'syncAutoRunState'),
extractFunction(source, 'isAutoRunLockedPhase'), extractFunction(source, 'isAutoRunLockedPhase'),
extractFunction(source, 'isAutoRunPausedPhase'), extractFunction(source, 'isAutoRunPausedPhase'),
extractFunction(source, 'isAutoRunScheduledPhase'),
extractFunction(source, 'isAutoRunSourceSyncPhase'), extractFunction(source, 'isAutoRunSourceSyncPhase'),
extractFunction(source, 'shouldSyncRunCountFromAutoRunSource'), extractFunction(source, 'shouldSyncRunCountFromAutoRunSource'),
extractFunction(source, 'applyAutoRunStatus'), extractFunction(source, 'applyAutoRunStatus'),
@@ -184,7 +180,6 @@ let currentAutoRun = {
currentRun: 0, currentRun: 0,
totalRuns: 1, totalRuns: 1,
attemptRun: 0, attemptRun: 0,
scheduledAt: null,
countdownAt: null, countdownAt: null,
countdownTitle: '', countdownTitle: '',
countdownNote: '', countdownNote: '',
@@ -205,9 +200,8 @@ function getLockedRunCountFromEmailPool() { return 0; }
function isCustomMailProvider() { return false; } function isCustomMailProvider() { return false; }
function usesCustomEmailPoolGenerator() { return false; } function usesCustomEmailPoolGenerator() { return false; }
function setDefaultAutoRunButton() {} function setDefaultAutoRunButton() {}
function updateAutoDelayInputState() {}
function updateFallbackThreadIntervalInputState() {} function updateFallbackThreadIntervalInputState() {}
function syncScheduledCountdownTicker() {} function syncAutoRunCountdownTicker() {}
function updateStopButtonState() {} function updateStopButtonState() {}
function getStepStatuses() { return {}; } function getStepStatuses() { return {}; }
function updateConfigMenuControls() {} function updateConfigMenuControls() {}
@@ -73,8 +73,6 @@ const inputAutoSkipFailures = { checked: false };
const inputContributionNickname = { value: 'tester' }; const inputContributionNickname = { value: 'tester' };
const inputContributionQq = { value: '123456' }; const inputContributionQq = { value: '123456' };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' }; const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const btnAutoRun = { disabled: false, innerHTML: '' }; const btnAutoRun = { disabled: false, innerHTML: '' };
const inputRunCount = { disabled: false }; const inputRunCount = { disabled: false };
const selectFlow = { value: latestState.activeFlowId }; const selectFlow = { value: latestState.activeFlowId };
@@ -132,7 +130,6 @@ function shouldWarnAutoRunFallbackRisk() { return false; }
function isAutoRunFallbackRiskPromptDismissed() { return false; } function isAutoRunFallbackRiskPromptDismissed() { return false; }
async function openAutoRunFallbackRiskConfirmModal() { throw new Error('should not be called'); } async function openAutoRunFallbackRiskConfirmModal() { throw new Error('should not be called'); }
function setAutoRunFallbackRiskPromptDismissed() {} function setAutoRunFallbackRiskPromptDismissed() {}
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
async function refreshContributionContentHint() { async function refreshContributionContentHint() {
events.push({ type: 'refresh' }); events.push({ type: 'refresh' });
${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'} ${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'}
@@ -272,8 +269,6 @@ const inputAutoSkipFailures = { checked: false };
const inputContributionNickname = { value: 'tester' }; const inputContributionNickname = { value: 'tester' };
const inputContributionQq = { value: '123456' }; const inputContributionQq = { value: '123456' };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' }; const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const btnAutoRun = { disabled: false, innerHTML: '' }; const btnAutoRun = { disabled: false, innerHTML: '' };
const inputRunCount = { disabled: false, value: '1' }; const inputRunCount = { disabled: false, value: '1' };
const inputPhoneVerificationEnabled = { checked: true }; const inputPhoneVerificationEnabled = { checked: true };
@@ -324,7 +319,6 @@ function shouldWarnAutoRunFallbackRisk() { return false; }
function isAutoRunFallbackRiskPromptDismissed() { return false; } function isAutoRunFallbackRiskPromptDismissed() { return false; }
async function openAutoRunFallbackRiskConfirmModal() { throw new Error('should not be called'); } async function openAutoRunFallbackRiskConfirmModal() { throw new Error('should not be called'); }
function setAutoRunFallbackRiskPromptDismissed() {} function setAutoRunFallbackRiskPromptDismissed() {}
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
async function refreshContributionContentHint() { async function refreshContributionContentHint() {
events.push({ type: 'refresh' }); events.push({ type: 'refresh' });
return null; return null;
@@ -247,10 +247,7 @@ const inputTempEmailReceiveMailbox = { value: 'relay@example.com' };
const inputTempEmailUseRandomSubdomain = { checked: true }; const inputTempEmailUseRandomSubdomain = { checked: true };
const inputAutoSkipFailures = { checked: false }; const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' }; const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
const inputAutoDelayEnabled = { checked: true };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '10' }; const inputAutoStepDelaySeconds = { value: '10' };
const inputOAuthFlowTimeoutEnabled = { checked: true };
const inputVerificationResendCount = { value: '6' }; const inputVerificationResendCount = { value: '6' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4; const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms'; 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 normalizeCloudflareTempEmailBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); } function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; } function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); } function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; } 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; } 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', () => { test('sidepanel html exposes flow selector and kiro source fields', () => {
[ [
'id="select-flow"', 'id="select-flow"',
'<option value="grok">Grok</option>',
'id="label-source-selector"', 'id="label-source-selector"',
'id="btn-open-webchat2api-github"',
'id="row-step6-cookie-settings"', '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="row-kiro-rs-url"',
'id="btn-open-kiro-rs-github"', 'id="btn-open-kiro-rs-github"',
'id="row-kiro-rs-key"', '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-web-status"',
'id="row-kiro-login-url"', 'id="row-kiro-login-url"',
'id="row-kiro-upload-status"', 'id="row-kiro-upload-status"',
'id="row-grok-register-status"',
'id="row-grok-sso-status"',
'id="row-grok-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) => { ].forEach((snippet) => {
assert.match(sidepanelHtml, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); 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', () => { 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/); 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', () => { test('sidepanel step definitions rerender when active flow changes even if plus/signup settings stay the same', () => {
const bundle = [ const bundle = [
extractFunction(sidepanelSource, 'normalizeSignupMethod'), extractFunction(sidepanelSource, 'normalizeSignupMethod'),
@@ -80,6 +181,7 @@ let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal'; let currentPlusPaymentMethod = 'paypal';
let currentPlusAccountAccessStrategy = 'oauth'; let currentPlusAccountAccessStrategy = 'oauth';
let currentSignupMethod = 'email'; let currentSignupMethod = 'email';
let currentPhoneVerificationEnabled = false;
let currentPhoneSignupReloginAfterBindEmailEnabled = false; let currentPhoneSignupReloginAfterBindEmailEnabled = false;
let currentStepDefinitionFlowId = 'openai'; let currentStepDefinitionFlowId = 'openai';
const DEFAULT_ACTIVE_FLOW_ID = 'openai'; const DEFAULT_ACTIVE_FLOW_ID = 'openai';
@@ -122,6 +224,7 @@ return {
plusPaymentMethod: 'paypal', plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'oauth', plusAccountAccessStrategy: 'oauth',
signupMethod: 'email', signupMethod: 'email',
phoneVerificationEnabled: false,
phoneSignupReloginAfterBindEmailEnabled: false, phoneSignupReloginAfterBindEmailEnabled: false,
accountContributionEnabled: false, accountContributionEnabled: false,
}, },
+1 -9
View File
@@ -132,10 +132,7 @@ const inputTempEmailReceiveMailbox = { value: '' };
const inputTempEmailUseRandomSubdomain = { checked: false }; const inputTempEmailUseRandomSubdomain = { checked: false };
const inputAutoSkipFailures = { checked: false }; const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' }; const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' }; const inputAutoStepDelaySeconds = { value: '' };
const inputOAuthFlowTimeoutEnabled = { checked: true };
const inputVerificationResendCount = { value: '4' }; const inputVerificationResendCount = { value: '4' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4; const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms'; 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 normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); } function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; } function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); } function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; } 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; } 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 inputLuckmailDomain = { value: '' };
const inputAutoSkipFailures = { checked: false }; const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '' }; const inputAutoSkipFailuresThreadIntervalMinutes = { value: '' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '' };
const inputAutoStepDelaySeconds = { value: '' }; const inputAutoStepDelaySeconds = { value: '' };
const inputOAuthFlowTimeoutEnabled = { checked: true };
const inputVerificationResendCount = { value: '' }; const inputVerificationResendCount = { value: '' };
const inputPhoneVerificationEnabled = { checked: false }; const inputPhoneVerificationEnabled = { checked: false };
const selectPhoneSmsProvider = { value: 'hero-sms' }; const selectPhoneSmsProvider = { value: 'hero-sms' };
@@ -483,7 +476,6 @@ function applyCloudflareTempEmailSettingsState() {}
function renderCloudflareDomainOptions() {} function renderCloudflareDomainOptions() {}
function setCloudflareDomainEditMode() {} function setCloudflareDomainEditMode() {}
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; } function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function formatAutoStepDelayInputValue(value) { return value == null ? '' : String(value); } function formatAutoStepDelayInputValue(value) { return value == null ? '' : String(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; } function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
function normalizeHeroSmsMaxPriceValue(value = '') { return String(value || '').trim(); } function normalizeHeroSmsMaxPriceValue(value = '') { return String(value || '').trim(); }
@@ -519,12 +511,12 @@ function updateHeroSmsPlatformDisplay() {}
function updatePhoneSmsProviderOrderSummary() {} function updatePhoneSmsProviderOrderSummary() {}
function applyAutoRunStatus() {} function applyAutoRunStatus() {}
function markSettingsDirty() {} function markSettingsDirty() {}
function updateAutoDelayInputState() {}
function updateFallbackThreadIntervalInputState() {} function updateFallbackThreadIntervalInputState() {}
function updateAccountRunHistorySettingsUI() {} function updateAccountRunHistorySettingsUI() {}
function updatePhoneVerificationSettingsUI() {} function updatePhoneVerificationSettingsUI() {}
function updatePanelModeUI() {} function updatePanelModeUI() {}
function updateMailProviderUI() { calls.push({ target: selectIcloudTargetMailboxType.value, provider: selectIcloudForwardMailProvider.value }); } function updateMailProviderUI() { calls.push({ target: selectIcloudTargetMailboxType.value, provider: selectIcloudForwardMailProvider.value }); }
function renderGrokRuntimeState() {}
function renderSub2ApiGroupOptions() {} function renderSub2ApiGroupOptions() {}
function isLuckmailProvider() { return false; } function isLuckmailProvider() { return false; }
function updateButtonStates() {} function updateButtonStates() {}
@@ -203,10 +203,7 @@ const inputTempEmailReceiveMailbox = { value: '' };
const inputTempEmailUseRandomSubdomain = { checked: false }; const inputTempEmailUseRandomSubdomain = { checked: false };
const inputAutoSkipFailures = { checked: false }; const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' }; const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' }; const inputAutoStepDelaySeconds = { value: '' };
const inputOAuthFlowTimeoutEnabled = { checked: true };
const inputVerificationResendCount = { value: '4' }; const inputVerificationResendCount = { value: '4' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4; const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms'; 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 normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); } function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; } function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); } function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; } 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; } 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}`); 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="row-operation-delay-settings"/);
assert.doesNotMatch(html, /id="input-operation-delay-enabled"/); 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 step6CookieIndex = html.indexOf('id="row-step6-cookie-settings"');
const autoDelayIndex = html.indexOf('id="row-auto-delay-settings"'); const sharedAutoRunIndex = html.indexOf('id="row-shared-auto-run"');
const oauthTimeoutIndex = html.indexOf('id="row-oauth-flow-timeout"'); const threadIntervalIndex = html.indexOf('id="row-auto-run-thread-interval"');
const stepRangeIndex = html.indexOf('id="row-step-execution-range"'); const stepRangeIndex = html.indexOf('id="row-step-execution-range"');
const oauthDisplayIndex = html.indexOf('id="row-oauth-display"'); 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(step6CookieIndex, -1);
assert.notEqual(autoDelayIndex, -1); assert.notEqual(sharedAutoRunIndex, -1);
assert.notEqual(oauthTimeoutIndex, -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(stepRangeIndex, -1);
assert.notEqual(oauthDisplayIndex, -1); assert.notEqual(oauthDisplayIndex, -1);
assert.ok(autoDelayIndex > step6CookieIndex, 'startup delay row should render below the openai step6 cookie row'); assert.notEqual(oauthCallbackIndex, -1);
assert.ok(stepRangeIndex > autoDelayIndex, 'step execution range should still remain below the startup delay row'); assert.notEqual(settingsActionsIndex, -1);
assert.ok(stepRangeIndex > oauthTimeoutIndex, 'step execution range should render below oauth timeout'); 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(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', () => { test('sidepanel operation delay state is always normalized back to enabled', () => {
@@ -713,7 +713,6 @@ function setFreePhoneReuseControlsLocked(locked) {
|| !Boolean(inputPhoneVerificationEnabled.checked && phoneVerificationSectionExpanded); || !Boolean(inputPhoneVerificationEnabled.checked && phoneVerificationSectionExpanded);
} }
function isAutoRunLockedPhase() { return false; } function isAutoRunLockedPhase() { return false; }
function isAutoRunScheduledPhase() { return false; }
${extractFunction('normalizeSignupMethod')} ${extractFunction('normalizeSignupMethod')}
${extractFunction('normalizeHeroSmsReuseEnabledValue')} ${extractFunction('normalizeHeroSmsReuseEnabledValue')}
@@ -979,8 +978,6 @@ const inputTempEmailReceiveMailbox = { value: '' };
const inputTempEmailUseRandomSubdomain = { checked: false }; const inputTempEmailUseRandomSubdomain = { checked: false };
const inputAutoSkipFailures = { checked: false }; const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' }; const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' }; const inputAutoStepDelaySeconds = { value: '' };
const inputPhoneVerificationEnabled = { checked: true }; const inputPhoneVerificationEnabled = { checked: true };
const inputFreePhoneReuseEnabled = { checked: true }; const inputFreePhoneReuseEnabled = { checked: true };
@@ -1072,7 +1069,6 @@ function normalizeCloudflareTempEmailBaseUrlValue(value) { return String(value |
function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); } function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); } function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; } function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); } function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; } 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; } 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; const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY; let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
let currentSignupMethod = 'email'; let currentSignupMethod = 'email';
let currentPhoneVerificationEnabled = false;
let currentPhoneSignupReloginAfterBindEmailEnabled = false; let currentPhoneSignupReloginAfterBindEmailEnabled = false;
const DEFAULT_SIGNUP_METHOD = 'email'; const DEFAULT_SIGNUP_METHOD = 'email';
let stepDefinitions = []; let stepDefinitions = [];
@@ -113,7 +114,7 @@ return {
assert.deepEqual(api.getStepIds(), [7]); assert.deepEqual(api.getStepIds(), [7]);
assert.deepEqual(api.calls[0], { assert.deepEqual(api.calls[0], {
type: 'getSteps', 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] }); 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; const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY; let currentPlusAccountAccessStrategy = DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY;
let currentSignupMethod = 'email'; let currentSignupMethod = 'email';
let currentPhoneVerificationEnabled = false;
let currentPhoneSignupReloginAfterBindEmailEnabled = false; let currentPhoneSignupReloginAfterBindEmailEnabled = false;
const DEFAULT_SIGNUP_METHOD = 'email'; const DEFAULT_SIGNUP_METHOD = 'email';
let stepDefinitions = []; let stepDefinitions = [];
@@ -424,7 +426,7 @@ return {
assert.deepEqual(api.getStepIds(), [13]); assert.deepEqual(api.getStepIds(), [13]);
assert.deepEqual(api.calls[0], { assert.deepEqual(api.calls[0], {
type: 'getSteps', 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\/settings-schema\.js/);
assert.match(source, /core\/flow-kernel\/source-registry\.js/); assert.match(source, /core\/flow-kernel\/source-registry\.js/);
assert.match(source, /shared\/kiro-timeouts\.js/); assert.match(source, /shared\/kiro-timeouts\.js/);
assert.match(source, /flows\/grok\/index\.js/);
assert.match(source, /flows\/grok\/workflow\.js/);
assert.match(source, /flows\/grok\/background\/state\.js/);
assert.match(source, /flows\/grok\/background\/register-runner\.js/);
assert.match(source, /flows\/grok\/mail-rules\.js/);
}); });
test('manifest loads shared source registry before content utils in static bundles', () => { test('manifest loads shared source registry before content utils in static bundles', () => {
@@ -41,15 +46,38 @@ test('manifest no longer ships a static Kiro content bundle', () => {
assert.equal(hasStaticKiroBundle, false); assert.equal(hasStaticKiroBundle, false);
}); });
test('manifest loads Grok flow definition in static bundles but not Grok content runtime', () => {
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
for (const entry of manifest.content_scripts || []) {
const scripts = Array.isArray(entry.js) ? entry.js : [];
if (!scripts.includes('flows/index.js')) continue;
assert.ok(scripts.includes('flows/kiro/index.js'));
assert.ok(scripts.includes('flows/grok/index.js'));
assert.ok(
scripts.indexOf('flows/kiro/index.js') < scripts.indexOf('flows/grok/index.js'),
'Kiro definition should load before Grok definition'
);
assert.ok(
scripts.indexOf('flows/grok/index.js') < scripts.indexOf('flows/index.js'),
'Grok definition must load before flows/index.js'
);
assert.equal(scripts.includes('flows/grok/content/register-page.js'), false);
}
});
test('background injects shared Kiro timeout module before Kiro content scripts', () => { test('background injects shared Kiro timeout module before Kiro content scripts', () => {
const source = fs.readFileSync('background.js', 'utf8'); const source = fs.readFileSync('background.js', 'utf8');
assert.match( assert.match(
source, source,
/const KIRO_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/register-page\.js'\];/ /const KIRO_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/register-page\.js'\];/
); );
assert.match( assert.match(
source, source,
/const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/desktop-authorize-page\.js'\];/ /const KIRO_DESKTOP_AUTHORIZE_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'shared\/kiro-timeouts\.js', 'content\/utils\.js', 'flows\/kiro\/content\/desktop-authorize-page\.js'\];/
);
assert.match(
source,
/const GROK_REGISTER_INJECT_FILES = \['flows\/openai\/index\.js', 'flows\/kiro\/index\.js', 'flows\/grok\/index\.js', 'flows\/index\.js', 'core\/flow-kernel\/flow-registry\.js', 'core\/flow-kernel\/source-registry\.js', 'content\/utils\.js', 'flows\/grok\/content\/register-page\.js'\];/
); );
}); });
@@ -83,6 +111,20 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
}), }),
'kiro-desktop-authorize' 'kiro-desktop-authorize'
); );
assert.equal(
registry.detectSourceFromLocation({
url: 'https://accounts.x.ai/sign-up',
hostname: 'accounts.x.ai',
}),
'grok-register-page'
);
assert.equal(
registry.detectSourceFromLocation({
url: 'https://grok.com/',
hostname: 'grok.com',
}),
'grok-register-page'
);
assert.equal( assert.equal(
registry.detectSourceFromLocation({ registry.detectSourceFromLocation({
url: 'https://example.com/', url: 'https://example.com/',
@@ -115,6 +157,22 @@ test('shared source registry exposes canonical Kiro sources and drivers', () =>
), ),
true true
); );
assert.equal(
registry.matchesSourceUrlFamily(
'grok-register-page',
'https://accounts.x.ai/sign-up',
'https://grok.com/'
),
true
);
assert.equal(
registry.matchesSourceUrlFamily(
'grok-register-page',
'https://grok.com/',
'https://accounts.x.ai/sign-up'
),
true
);
assert.equal( assert.equal(
registry.matchesSourceUrlFamily( registry.matchesSourceUrlFamily(
'kiro-desktop-authorize', 'kiro-desktop-authorize',
@@ -137,4 +195,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/register-runner', 'kiro-open-register-page'), true);
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/desktop-authorize-runner', 'kiro-start-desktop-authorize'), true); assert.equal(registry.driverAcceptsCommand('flows/kiro/background/desktop-authorize-runner', 'kiro-start-desktop-authorize'), true);
assert.equal(registry.driverAcceptsCommand('flows/kiro/background/publisher-kiro-rs', 'kiro-upload-credential'), true); assert.equal(registry.driverAcceptsCommand('flows/kiro/background/publisher-kiro-rs', 'kiro-upload-credential'), true);
assert.equal(registry.driverAcceptsCommand('flows/grok/content/register-page', 'grok-submit-profile'), true);
assert.equal(registry.driverAcceptsCommand('flows/grok/background/register-runner', 'grok-extract-sso-cookie'), true);
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 goPaySteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gopay' });
const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }); const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' });
const kiroSteps = api.getSteps({ activeFlowId: 'kiro' }); const kiroSteps = api.getSteps({ activeFlowId: 'kiro' });
const grokSteps = api.getSteps({ activeFlowId: 'grok' });
assert.equal(Array.isArray(steps), true); assert.equal(Array.isArray(steps), true);
assert.equal(steps.length, 11); assert.equal(steps.length, 11);
@@ -162,8 +163,9 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 19); assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 19);
assert.equal(api.hasFlow('openai'), true); assert.equal(api.hasFlow('openai'), true);
assert.equal(api.hasFlow('kiro'), true); assert.equal(api.hasFlow('kiro'), true);
assert.equal(api.hasFlow('grok'), true);
assert.equal(api.hasFlow('site-a'), false); assert.equal(api.hasFlow('site-a'), false);
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro']); assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai', 'kiro', 'grok']);
assert.deepStrictEqual(api.getSteps({ activeFlowId: 'site-a' }), []); assert.deepStrictEqual(api.getSteps({ activeFlowId: 'site-a' }), []);
assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null); assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null);
assert.deepStrictEqual( assert.deepStrictEqual(
@@ -210,6 +212,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[6].title, '创建 Plus Checkout');
assert.equal(plusSteps[8].title, 'PayPal 登录与授权'); 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']); 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', () => { test('Plus session strategy swaps the OAuth tail for a single SUB2API import node', () => {
const globalScope = {}; const globalScope = {};
const api = new Function('self', `${readStepDefinitionsBundle()}; return self.MultiPageStepDefinitions;`)(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); 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 api = new Function(`
const events = []; const events = [];
const listeners = new Map(); const listeners = new Map();
@@ -1147,22 +1147,31 @@ ${extractFunction('installStep5NavigationCompletionReporter')}
return { return {
run() { run() {
let completionCount = 0; const cleanup = installStep5NavigationCompletionReporter((payload) => {
const cleanup = installStep5NavigationCompletionReporter(() => { events.push({ type: 'complete', payload });
completionCount += 1;
events.push({ type: 'complete' });
}); });
const beforeUnload = listeners.get('beforeunload'); const beforeUnload = listeners.get('beforeunload');
if (beforeUnload) { if (beforeUnload) {
beforeUnload({ type: 'beforeunload' }); beforeUnload({ type: 'beforeunload' });
} }
cleanup(); cleanup();
return { completionCount, events }; return { events };
}, },
}; };
`)(); `)();
const result = api.run(); 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); 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 autoRunTotalRuns = 3;
let autoRunAttemptRun = 4; let autoRunAttemptRun = 4;
let autoRunSessionId = 99; let autoRunSessionId = 99;
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
const DEFAULT_STATE = { const DEFAULT_STATE = {
nodeStatuses: {}, nodeStatuses: {},
}; };
@@ -158,9 +157,6 @@ async function getState() {
function getPendingAutoRunTimerPlan() { function getPendingAutoRunTimerPlan() {
return null; return null;
} }
function isAutoRunScheduledState() {
return false;
}
function getStep8CallbackUrlFromNavigation() { return ''; } function getStep8CallbackUrlFromNavigation() { return ''; }
function getStep8CallbackUrlFromTabUpdate() { return ''; } function getStep8CallbackUrlFromTabUpdate() { return ''; }
function getStep8EffectLabel() { return 'no_effect'; } function getStep8EffectLabel() { return 'no_effect'; }
+43 -11
View File
@@ -10,10 +10,11 @@
## 1. 项目目标 ## 1. 项目目标
这是一个 Chrome 扩展,用于承载多套注册与认证自动化 flow。当前已注册条主 flow 这是一个 Chrome 扩展,用于承载多套注册与认证自动化 flow。当前已注册条主 flow
- `openai`OpenAI / ChatGPT OAuth 注册、登录、平台绑定与 Plus 扩展链路 - `openai`OpenAI / ChatGPT OAuth 注册、登录、平台绑定与 Plus 扩展链路
- `kiro`AWS Builder ID 注册页 + 桌面授权 + `kiro.rs` 凭据上传链路 - `kiro`AWS Builder ID 注册页 + 桌面授权 + `kiro.rs` 凭据上传链路
- `grok`Grok / xAI 注册页自动化 + `webchat2api` SSO Cookie 远程上传链路
它的核心价值不是“打开一个页面点几个按钮”,而是把下面这些环节串成一条完整可恢复的自动化链路: 它的核心价值不是“打开一个页面点几个按钮”,而是把下面这些环节串成一条完整可恢复的自动化链路:
@@ -28,7 +29,7 @@
- 自动确认 OAuth 同意页 - 自动确认 OAuth 同意页
- 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API - 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API
同时,这套扩展现在不再把“来源、步骤、侧边栏显隐、运行态字段”硬编码在单一 OpenAI 链路里,而是统一挂到 `flow registry + source registry + settings schema + step definitions` 这一层。Kiro 这类新 flow 可以复用共享邮箱服务与 IP 代理服务,但不会复用 OpenAI 的接码、Plus、平台绑定和贡献模式逻辑。 同时,这套扩展现在不再把“来源、步骤、侧边栏显隐、运行态字段”硬编码在单一 OpenAI 链路里,而是统一挂到 `flow registry + source registry + settings schema + step definitions` 这一层。Kiro / Grok 这类新 flow 可以复用共享邮箱服务与 IP 代理服务,但不会复用 OpenAI 的接码、Plus、平台绑定和贡献模式逻辑。
## 2. 核心运行参与者 ## 2. 核心运行参与者
@@ -43,8 +44,8 @@
- 向后台发送命令 - 向后台发送命令
- 接收后台广播并更新 UI - 接收后台广播并更新 UI
- 动态渲染步骤列表 - 动态渲染步骤列表
- 维护 `activeFlowId + targetId` 的双层选择;`openai` flow 继续把 `panelMode` 归一为 legacy integration target`kiro` flow 使用独立`flows.kiro.targetId` - 维护 `activeFlowId + targetId` 的双层选择;`openai` flow 继续把 `panelMode` 归一为 legacy integration target`kiro` / `grok` flow 使用各自`flows.<flowId>.targetId`
- 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态不显示 OpenAI 接码 / Plus / 平台绑定配置 - 按当前 flow capability 决定显隐分组;Kiro flow 只显示来源下拉、`kiro.rs URL / API Key`、共享邮箱服务、共享 IP 代理与 Kiro 运行状态Grok flow 只显示 `webchat2api` URL / 管理密钥、SSO 提取与上传状态、共享账号密码、共享邮箱服务和共享 IP 代理;二者都不显示 OpenAI 接码 / Plus / 平台绑定配置
- 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源 - 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源
- 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态 - 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态
- 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `flowpilot.qlhazycoder.top` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现 - 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `flowpilot.qlhazycoder.top` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现
@@ -150,13 +151,13 @@
- `runtimeState`:当前运行中的 node、nodeStatuses、activeRunId 等核心执行态 - `runtimeState`:当前运行中的 node、nodeStatuses、activeRunId 等核心执行态
- `sharedState`automation window、tab registry、最近来源 URL 这类跨 flow 共享运行态 - `sharedState`automation window、tab registry、最近来源 URL 这类跨 flow 共享运行态
- `serviceState`:代理、邮箱等共享服务运行态 - `serviceState`:代理、邮箱等共享服务运行态
- `flowState`OpenAI / Kiro 各自的 flow 私有运行态 - `flowState`OpenAI / Kiro / Grok 各自的 flow 私有运行态
这两个模块的作用,是把“当前该跑哪些节点”“当前 state 应该怎么看”“前后台恢复时如何合并 patch”从 `background.js` 主入口里抽离出来,避免再次退回到旧的平铺 step 状态机。 这两个模块的作用,是把“当前该跑哪些节点”“当前 state 应该怎么看”“前后台恢复时如何合并 patch”从 `background.js` 主入口里抽离出来,避免再次退回到旧的平铺 step 状态机。
### 3.4 步骤注册 ### 3.4 步骤注册
[data/step-definitions.js](./data/step-definitions.js) 提供共享步骤元数据,并按当前 `activeFlowId` 输出 flow-specific workflow。`openai` flow 会继续按 `plusPaymentMethod / signupMethod / plusAccountAccessStrategy` 动态解析步骤标题与节点集合:当 Plus 模式为邮箱注册且来源支持时,原本的 OAuth 尾链可以被替换成 `sub2api-session-import``cpa-session-import``kiro` flow 输出固定的 9 节点注册/桌面授权/上传 workflow。 [data/step-definitions.js](./data/step-definitions.js) 提供共享步骤元数据,并按当前 `activeFlowId` 输出 flow-specific workflow。`openai` flow 会继续按 `plusPaymentMethod / signupMethod / plusAccountAccessStrategy` 动态解析步骤标题与节点集合:当 Plus 模式为邮箱注册且来源支持时,原本的 OAuth 尾链可以被替换成 `sub2api-session-import``cpa-session-import``kiro` flow 输出固定的 9 节点注册/桌面授权/上传 workflow`grok` flow 输出固定的 6 节点注册、SSO Cookie 提取与 `webchat2api` 上传 workflow。
[core/flow-kernel/step-registry.js](./core/flow-kernel/step-registry.js) 负责把“workflow node 元数据”映射到“步骤执行器”,同时保留 `flowId / nodeId / displayOrder / executeKey` 等稳定节点元数据。 [core/flow-kernel/step-registry.js](./core/flow-kernel/step-registry.js) 负责把“workflow node 元数据”映射到“步骤执行器”,同时保留 `flowId / nodeId / displayOrder / executeKey` 等稳定节点元数据。
这意味着: 这意味着:
@@ -168,7 +169,7 @@
### 3.4.1 验证码规则注册 ### 3.4.1 验证码规则注册
[background/mail-rule-registry.js](./background/mail-rule-registry.js) 负责按当前 `activeFlowId` 选择验证码规则构造器。[flows/openai/mail-rules.js](./flows/openai/mail-rules.js) 提供 OpenAI flow 当前使用的注册验证码 / 登录验证码规则。 [background/mail-rule-registry.js](./background/mail-rule-registry.js) 负责按当前 `activeFlowId` 选择验证码规则构造器。[flows/openai/mail-rules.js](./flows/openai/mail-rules.js) 提供 OpenAI flow 当前使用的注册验证码 / 登录验证码规则[flows/kiro/mail-rules.js](./flows/kiro/mail-rules.js) 提供 Kiro / AWS Builder ID 注册验证码与桌面授权验证码规则,[flows/grok/mail-rules.js](./flows/grok/mail-rules.js) 提供 xAI / Grok 注册验证码规则
这层统一产出: 这层统一产出:
@@ -180,7 +181,8 @@
它的职责边界是: 它的职责边界是:
- `background/verification-flow.js` 只消费统一规则 payload,不再在每个 provider 或内容脚本里各自硬写一套 OpenAI 验证码过滤条件。 - `background/verification-flow.js` 只消费统一规则 payload,不再在每个 provider 或内容脚本里各自硬写一套 OpenAI 验证码过滤条件。
- “邮件从哪里读”由 provider 决定,“哪封邮件算命中验证码”优先由规则注册层决定 - `background/flow-mail-polling.js` 负责共享的 provider 分派、iCloud / 2925 会话准备、网页邮箱标签页打开与内容脚本轮询;flow runner 只提供 `flowId / nodeId / step / filterAfterTimestamp` 这类业务上下文
- “邮件从哪里读”由公共轮询调度层和 provider 决定,“哪封邮件算命中验证码”优先由规则注册层决定。
- 新 flow 如需接入邮箱验证码,应优先新增自己的 mail rules builder,而不是继续改写 OpenAI 规则。 - 新 flow 如需接入邮箱验证码,应优先新增自己的 mail rules builder,而不是继续改写 OpenAI 规则。
### 3.5 日志步骤号链路 ### 3.5 日志步骤号链路
@@ -214,6 +216,7 @@
- `contributionLastPollAt` - `contributionLastPollAt`
- OAuth 链接 - OAuth 链接
- Kiro 运行态:使用 `kiroRuntime.session / register / webAuth / desktopAuth / upload` 命名空间,保存 Kiro 官方登录入口、Builder ID 注册页状态、Kiro Web 登录态摘要、桌面授权 PKCE 凭据、上传状态和当前 Kiro 会话进度 - Kiro 运行态:使用 `kiroRuntime.session / register / webAuth / desktopAuth / upload` 命名空间,保存 Kiro 官方登录入口、Builder ID 注册页状态、Kiro Web 登录态摘要、桌面授权 PKCE 凭据、上传状态和当前 Kiro 会话进度
- Grok 运行态:权威状态位于 `runtimeState.flowState.grok`,包含 `session / register / sso / upload`;顶层 `grokRegisterTabId / grokPageState / grokEmail / grokPassword / grokSsoCookie / grokSsoCookies / grokWebchat2ApiUpload*` 只作为兼容投影,不应当再形成第二套状态机;兼容 flat 字段只有非空值才能覆盖 canonical runtime,避免空旧字段反向覆盖本轮 SSO
- 当前轮冻结后的注册方式 `resolvedSignupMethod` - 当前轮冻结后的注册方式 `resolvedSignupMethod`
- 当前统一账号标识 `accountIdentifierType / accountIdentifier` - 当前统一账号标识 `accountIdentifierType / accountIdentifier`
- 当前邮箱 / 密码 - 当前邮箱 / 密码
@@ -278,7 +281,7 @@
- Kiro 目标配置:`flows.kiro.targetId``flows.kiro.targets["kiro-rs"] = { baseUrl, apiKey }` - Kiro 目标配置:`flows.kiro.targetId``flows.kiro.targets["kiro-rs"] = { baseUrl, apiKey }`
- 自动运行默认配置 - 自动运行默认配置
- 操作间延迟兼容字段 `operationDelayEnabled`:当前会被严格归一为启用;sidepanel 不再暴露真正关闭入口,但内容脚本仍通过 `chrome.storage.local` 监听该字段完成兼容同步 - 操作间延迟兼容字段 `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。 - flow 级执行范围配置 `stepExecutionRangeByFlow`:按 flowId 保存,例如 `openai: { enabled: true, fromStep: 3, toStep: 6 }`;侧栏中的 `codex` 会归一到内部默认 flowId `openai`。这是通用配置结构,但当前只给 codex/openai flow 暴露 UI。
- 账号运行历史 `accountRunHistory`(以统一账号标识为主键;邮箱账号保留邮箱主键,手机号注册保留手机号主键;同一轮中后续绑定的邮箱或手机号会写入同一条记录的副身份字段,保存最近一次状态:成功/失败/停止) - 账号运行历史 `accountRunHistory`(以统一账号标识为主键;邮箱账号保留邮箱主键,手机号注册保留手机号主键;同一轮中后续绑定的邮箱或手机号会写入同一条记录的副身份字段,保存最近一次状态:成功/失败/停止)
- 账号运行历史本地同步 helper 地址 - 账号运行历史本地同步 helper 地址
@@ -762,9 +765,34 @@ Kiro flow 的目标不是复用 OpenAI 注册链,而是独立完成“Kiro 官
- Kiro 只复用共享账户密码、共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路 - Kiro 只复用共享账户密码、共享邮箱服务和共享 IP 代理服务,不复用 OpenAI 接码 / Plus / 平台绑定 / 贡献模式链路
- Kiro 自动运行走通用 linear node runner,但执行器、运行态、页面驱动和上传器全部独立在 `background/kiro/*``content/kiro/*` - Kiro 自动运行走通用 linear node runner,但执行器、运行态、页面驱动和上传器全部独立在 `background/kiro/*``content/kiro/*`
- Kiro 运行态统一落在 `kiroRuntime` 命名空间,不再散落为多个平铺字段 - Kiro 运行态统一落在 `kiroRuntime` 命名空间,不再散落为多个平铺字段
- Kiro 注册验证码与桌面授权验证码只在 runner 内计算业务时间窗,具体 AWS 邮件匹配规则由 `flows/kiro/mail-rules.js` 提供,provider 分派统一走 `background/flow-mail-polling.js`
- `kiro.rs` 上传固定发送 BuilderId profileArn `arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX`machineId 固定按 `sha256("KotlinNativeAPI/" + refreshToken)` 生成 - `kiro.rs` 上传固定发送 BuilderId profileArn `arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX`machineId 固定按 `sha256("KotlinNativeAPI/" + refreshToken)` 生成
- Kiro 注册页覆盖 `app.kiro.dev` 与 AWS Builder ID 页面;桌面授权页只绑定 AWS authorize 页面,二者由后台按 source 动态注入,避免同域 AWS 页面被错误归到另一条 Kiro 子链路 - Kiro 注册页覆盖 `app.kiro.dev` 与 AWS Builder ID 页面;桌面授权页只绑定 AWS authorize 页面,二者由后台按 source 动态注入,避免同域 AWS 页面被错误归到另一条 Kiro 子链路
## 6.3 Grok Flow 链路
Grok flow 的目标是完成 Grok / xAI 注册页自动化,并把本轮提取到的 `sso` Cookie 上传到 `webchat2api` 的远程账号注入接口。它不是 OpenAI 目标来源,也不是公开贡献 / 发布 flow。
链路如下:
1. sidepanel 把当前 flow 切到 `grok`target 固定为 `webchat2api`
2. 步骤 1 打开 Grok / xAI 注册入口,并清理本轮注册页运行态
3. 步骤 2 通过共享邮箱服务生成或复用注册邮箱,并提交到 xAI 登录/注册页
4. 步骤 3 调用 `background/flow-mail-polling.js`,由 `flows/grok/mail-rules.js` 构造 xAI / Grok 验证码规则并轮询邮箱验证码,再交给 Grok 内容脚本提交
5. 步骤 4 填写资料和密码,推进注册完成页状态
6. 步骤 5 先通过 `chrome.cookies.get``x.ai / grok.com / accounts.x.ai` 查找 `sso` Cookie,必要时 fallback 到 Grok 内容脚本读取 `document.cookie`;提取到的当前值保存到 `runtimeState.flowState.grok.sso` 并投影到 `grokSsoCookie / grokSsoCookies`,其中 `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. 邮箱与 provider 链路
### 7.1 共享邮箱生成补充 ### 7.1 共享邮箱生成补充
@@ -840,8 +868,11 @@ Kiro flow 的目标不是复用 OpenAI 注册链,而是独立完成“Kiro 官
- `background/registration-email-state.js` - `background/registration-email-state.js`
统一维护当前注册邮箱、上一份比较基线邮箱、来源与更新时间,并提供 Duck 生成前解析比较基线、Step 8 清空当前邮箱但保留历史基线,以及 Step 8 `add-email` 写入邮箱时保留手机号身份的共享工具。 统一维护当前注册邮箱、上一份比较基线邮箱、来源与更新时间,并提供 Duck 生成前解析比较基线、Step 8 清空当前邮箱但保留历史基线,以及 Step 8 `add-email` 写入邮箱时保留手机号身份的共享工具。
- `background/mail-rule-registry.js` + `flows/openai/mail-rules.js` - `background/mail-rule-registry.js` + `flows/*/mail-rules.js`
统一构造 Step 4 / Step 8 的验证码规则 payload,区分注册验证码与登录/绑定验证码,并把 `senderFilters / subjectFilters / codePatterns / targetEmailHints / mail2925MatchTargetEmail` 收敛到同一层。 统一构造各 flow 的验证码规则 payload,区分 OpenAI 注册/登录验证码、Kiro 注册/桌面授权验证码与 Grok 注册验证码,并把 `senderFilters / subjectFilters / codePatterns / targetEmailHints / mail2925MatchTargetEmail` 收敛到 flow mail rules 层。
- `background/flow-mail-polling.js`
统一执行 flow 邮箱验证码轮询调度:读取当前邮箱配置,调用 `mailRuleRegistry.buildVerificationPollPayloadForNode`,分派 Hotmail / LuckMail / Cloudflare Temp Email / Cloud Mail / YYDS Mail 这类 API provider,并为 iCloud、2925 和普通网页邮箱准备会话与内容脚本轮询。Kiro 与后续新 flow 不应在各自 runner 内复制 provider 分派逻辑。
- `background/generated-email-helpers.js` - `background/generated-email-helpers.js`
在原有 Duck / Cloudflare / iCloud / Cloudflare Temp Email 生成链路之外,新增 Gmail / 2925 的共享生成接入;Duck 生成前会按“侧栏当前可见邮箱 -> `registrationEmailState.current` -> `registrationEmailState.previous`”的顺序解析比较基线。 在原有 Duck / Cloudflare / iCloud / Cloudflare Temp Email 生成链路之外,新增 Gmail / 2925 的共享生成接入;Duck 生成前会按“侧栏当前可见邮箱 -> `registrationEmailState.current` -> `registrationEmailState.previous`”的顺序解析比较基线。
@@ -1131,6 +1162,7 @@ Hide My Email 获取与管理链路:
- 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态 - 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态
- 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态 - 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态
- fresh-attempt reset 会保留 `stepExecutionRangeByFlow`,避免重置运行态时丢失用户设置的执行窗口 - fresh-attempt reset 会保留 `stepExecutionRangeByFlow`,避免重置运行态时丢失用户设置的执行窗口
- Grok fresh-attempt reset 会清理注册页 runtime、当前 SSO 与 upload 状态,避免下一轮误上传上一轮旧 SSO
5. 执行 `runAutoSequenceFromStep` 5. 执行 `runAutoSequenceFromStep`
- 如果当前 `activeFlowId !== openai`,后台会切到通用 linear node runner,按当前 flow 的 workflow node 顺序执行,并复用统一的完成状态与 idle watchdog - 如果当前 `activeFlowId !== openai`,后台会切到通用 linear node runner,按当前 flow 的 workflow node 顺序执行,并复用统一的完成状态与 idle watchdog
- 如果 `stepExecutionRangeByFlow` 已启用,自动运行只会遍历允许范围内的 workflow node;范围外节点会被跳过,`getFirstUnfinishedNodeId` 与保存进度判断也只统计允许节点 - 如果 `stepExecutionRangeByFlow` 已启用,自动运行只会遍历允许范围内的 workflow node;范围外节点会被跳过,`getFirstUnfinishedNodeId` 与保存进度判断也只统计允许节点
+3
View File
@@ -171,6 +171,7 @@
- `resolvedSignupMethod` 是当前轮冻结结果,不等同于用户此刻 UI 上选择的 `signupMethod` - `resolvedSignupMethod` 是当前轮冻结结果,不等同于用户此刻 UI 上选择的 `signupMethod`
- 强制绑定邮箱后重登用邮箱身份,只能通过单次执行参数覆盖登录身份,不能持久改写 `signupMethod` - 强制绑定邮箱后重登用邮箱身份,只能通过单次执行参数覆盖登录身份,不能持久改写 `signupMethod`
- flow 能力不足时必须在步骤定义层或启动校验层处理,不能等执行到不存在的节点后才报错。 - flow 能力不足时必须在步骤定义层或启动校验层处理,不能等执行到不存在的节点后才报错。
- flow 私有远程上传、发布型 flow、贡献型 flow 必须显式区分:只有声明了 `publicationTargets` 或明确支持贡献能力的 flow 才能进入发布/贡献 adapter 校验;例如 Grok / `webchat2api` SSO Cookie 上传属于 Grok flow 自己的收尾节点,不是 publication target,也不能因为有 target 就默认接入贡献模式。
### 1.7 日志步骤号原则 ### 1.7 日志步骤号原则
@@ -285,6 +286,7 @@
- 贡献流程的后台公开 OAuth 状态机应优先收敛到独立模块,例如 `background/contribution-oauth.js` - 贡献流程的后台公开 OAuth 状态机应优先收敛到独立模块,例如 `background/contribution-oauth.js`
- 贡献模式的侧栏按钮、状态展示和轮询调度应优先收敛到独立 manager,例如 `sidepanel/contribution-mode.js` - 贡献模式的侧栏按钮、状态展示和轮询调度应优先收敛到独立 manager,例如 `sidepanel/contribution-mode.js`
- 如果服务端当前返回“无需手动提交 callback”,扩展端必须把它当兼容成功态处理,不能简单按 HTTP 非 200 直接视为失败 - 如果服务端当前返回“无需手动提交 callback”,扩展端必须把它当兼容成功态处理,不能简单按 HTTP 非 200 直接视为失败
- 如果新增 flow 只是私有收尾产物,例如 Cookie、JSON、会话文件、手动复制值,或只上传到该 flow 自己的专属远端服务,必须保持 `publicationTargets` 为空并关闭 `supportsAccountContribution`;如果后续产品决定把它纳入正式发布 / 贡献体系,必须先设计 publication target、贡献 adapter、敏感字段脱敏和测试,再打开能力开关。
### 3.4.1 iCloud Hide My Email 维护补充 ### 3.4.1 iCloud Hide My Email 维护补充
@@ -495,6 +497,7 @@ npm test
15. 如果创建了 `docs/md/` 方案文件,我有没有确认它是否应被提交? 15. 如果创建了 `docs/md/` 方案文件,我有没有确认它是否应被提交?
16. 如果改动涉及 flow、步骤、模式切换,我有没有同时检查 `getSteps / getNodes / getWorkflow`、后台 registry、sidepanel `workflowNodes` 和 node 状态? 16. 如果改动涉及 flow、步骤、模式切换,我有没有同时检查 `getSteps / getNodes / getWorkflow`、后台 registry、sidepanel `workflowNodes` 和 node 状态?
17. 我有没有新增任何用数字步骤判断业务含义的代码?如果有,是否能改为 `key / nodeId` 17. 我有没有新增任何用数字步骤判断业务含义的代码?如果有,是否能改为 `key / nodeId`
18. 如果新增或调整 flow target,我有没有确认它到底是 flow 私有收尾目标、正式发布目标还是贡献 flow,并同步检查 `publicationTargets / supportsAccountContribution / contributionAdapterIds / shared/contribution-registry.js`
## 9. 完成标准 ## 9. 完成标准
+23 -8
View File
@@ -21,7 +21,7 @@
- `.gitignore`:定义仓库忽略规则,当前忽略 `docs/md/``.github/``_metadata/``.vscode/` 等目录。 - `.gitignore`:定义仓库忽略规则,当前忽略 `docs/md/``.github/``_metadata/``.vscode/` 等目录。
- `LICENSE`:项目许可证文件。 - `LICENSE`:项目许可证文件。
- `README.md`:面向使用者的精简说明文档,主要介绍项目用途、功能范围、快速开始与文档入口;不再承载过多技术实现细节。 - `README.md`:面向使用者的精简说明文档,主要介绍项目用途、功能范围、快速开始与文档入口;不再承载过多技术实现细节。
- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前已按 `activeFlowId` 装配 flow-aware 的步骤定义、执行注册表、自动运行与状态同步,`openai` flow 继续承接 `oauthFlowTimeoutEnabled` `stepExecutionRangeByFlow``kiro` flow 走独立的“注册页 1-6 步 -> 桌面授权 7-8 步 -> `kiro.rs` 上传 9 步”链路。 - `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前已按 `activeFlowId` 装配 flow-aware 的步骤定义、执行注册表、自动运行与状态同步,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 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。 - `cloudmail-utils.js`Cloud Mail / SkyMail 相关的纯工具函数,负责 API 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。
- `cloudflare-temp-email-utils.js`Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。 - `cloudflare-temp-email-utils.js`Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。
- `gopay-utils.js`GoPay / GPC Plus 支付相关纯工具函数,负责支付方式归一化、GoPay 手机号/OTP/PIN 归一化、GPC API 地址归一化、API Key 请求头、任务创建/查询/OTP/PIN/停止 URL 与余额响应解析。 - `gopay-utils.js`GoPay / GPC Plus 支付相关纯工具函数,负责支付方式归一化、GoPay 手机号/OTP/PIN 归一化、GPC API 地址归一化、API Key 请求头、任务创建/查询/OTP/PIN/停止 URL 与余额响应解析。
@@ -59,12 +59,16 @@
- `flows/kiro/background/desktop-client.js`:Kiro 桌面授权协议层,负责桌面 OIDC client 注册、PKCE 参数生成、授权地址组装、callback 参数校验和授权码换 token。 - `flows/kiro/background/desktop-client.js`:Kiro 桌面授权协议层,负责桌面 OIDC client 注册、PKCE 参数生成、授权地址组装、callback 参数校验和授权码换 token。
- `flows/kiro/background/desktop-authorize-runner.js`:Kiro 桌面授权执行器,负责步骤 7~8 的标签页打开、授权页轮询、localhost callback 捕获,以及桌面授权完成后的凭据落库。 - `flows/kiro/background/desktop-authorize-runner.js`:Kiro 桌面授权执行器,负责步骤 7~8 的标签页打开、授权页轮询、localhost callback 捕获,以及桌面授权完成后的凭据落库。
- `flows/kiro/background/publisher-kiro-rs.js`Kiro 发布器,负责 `kiro.rs` 地址归一化、连通性探测、BuilderId profileArn 固定映射、machineId 计算、上传 payload 构建与最终凭据上传。 - `flows/kiro/background/publisher-kiro-rs.js`Kiro 发布器,负责 `kiro.rs` 地址归一化、连通性探测、BuilderId profileArn 固定映射、machineId 计算、上传 payload 构建与最终凭据上传。
- `flows/grok/background/state.js`:Grok 独立运行态模型与状态工具,负责 `runtimeState.flowState.grok.session / register / sso / 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-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。
- `background/ip-proxy-provider-711proxy.js`711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。 - `background/ip-proxy-provider-711proxy.js`711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;日志条目统一写入结构化 `step / stepKey`,sidepanel 只读取该元数据渲染步骤标签,不再从日志正文解析步骤号;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。 - `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;日志条目统一写入结构化 `step / stepKey`,sidepanel 只读取该元数据渲染步骤标签,不再从日志正文解析步骤号;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。
- `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。 - `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
- `background/mail-rule-registry.js`flow-aware 邮件规则注册表,负责按 `activeFlowId` 选择验证码规则构造器,并统一输出注册/登录验证码轮询 payload。 - `background/mail-rule-registry.js`flow-aware 邮件规则注册表,负责按 `activeFlowId` 选择验证码规则构造器,并统一输出注册/登录验证码轮询 payload。
- `background/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` 写入邮箱时按需保留手机号身份”的统一工具。 - `background/registration-email-state.js`:注册邮箱运行态共享模块,负责维护 `registrationEmailState = { current, previous, source, updatedAt }`,并提供“当前邮箱清空时保留上一比较基线”“Duck 生成前解析比较基线”“Step 8 `add-email` 写入邮箱时按需保留手机号身份”的统一工具。
- `core/flow-kernel/runtime-state.js`:运行态视图与 patch 构造模块,负责把会话字段归一到 `runtimeState / sharedState / serviceState / flowState` 结构,并维护 flow-aware 的 session patch、节点状态默认值与兼容视图输出。 - `core/flow-kernel/runtime-state.js`:运行态视图与 patch 构造模块,负责把会话字段归一到 `runtimeState / sharedState / serviceState / flowState` 结构,并维护 flow-aware 的 session patch、节点状态默认值与兼容视图输出。
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。 - `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
@@ -80,7 +84,7 @@
## `background/steps/` ## `background/steps/`
- `flows/openai/background/steps/wait-registration-success.js`:步骤 6 实现,负责注册资料提交后等待 20 秒,让注册成功状态和页面跳转稳定;默认不清理 cookies,只有侧栏第六步 `清 Cookies` 开关开启时才会在等待结束后清理 ChatGPT / OpenAI 相关 cookies。 - `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/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-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 账号已自动登录。 - `flows/openai/background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;邮箱注册继续走邮箱验证码,手机号注册改走短信验证码;当 provider 为 2925 时,会在邮箱轮询前先确保当前 2925 账号已自动登录。
@@ -152,6 +156,11 @@
## `flows/` ## `flows/`
- `flows/openai/mail-rules.js`OpenAI flow 的验证码邮件规则定义,负责按注册/登录节点输出 code pattern、关键词、目标邮箱提示、2925 receive 弱匹配与轮询参数。 - `flows/openai/mail-rules.js`OpenAI flow 的验证码邮件规则定义,负责按注册/登录节点输出 code pattern、关键词、目标邮箱提示、2925 receive 弱匹配与轮询参数。
- `flows/kiro/mail-rules.js`Kiro flow 的 AWS Builder ID 邮件规则定义,负责按注册验证码节点与桌面授权验证码节点输出 AWS 发件人、主题、关键词、code pattern、目标邮箱提示、2925 receive 弱匹配与轮询参数。
- `flows/grok/index.js`Grok flow 定义,声明 `webchat2api` target、Grok 注册页 runtime source、Grok 专属设置分组、能力边界与 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/` ## `icons/`
@@ -178,9 +187,10 @@
## `shared/` ## `shared/`
- `core/flow-kernel/flow-capabilities.js`:flow 能力矩阵归一化模块,负责根据 `activeFlowId` 与来源解析 sidepanel 的显隐能力、来源作用域、贡献模式边界、步骤范围 UI 作用域,以及 Plus 模式下 `账号接入策略` 的可选项、可编辑性与最终生效值。 - `core/flow-kernel/flow-capabilities.js`:flow 能力矩阵归一化模块,负责根据 `activeFlowId` 与来源解析 sidepanel 的显隐能力、来源作用域、贡献模式边界、步骤范围 UI 作用域,以及 Plus 模式下 `账号接入策略` 的可选项、可编辑性与最终生效值。
- `core/flow-kernel/flow-registry.js`flow/source/settings group 总注册表,定义 `openai / kiro` 套 flow、各自 target/runtime source、可见分组、driver 定义,以及默认 target 和默认 `kiro.rs` 配置。 - `core/flow-kernel/flow-registry.js`flow/source/settings group 总注册表,定义 `openai / kiro / grok` 套 flow、各自 target/runtime source、可见分组、driver 定义,以及默认 target 和默认 `kiro.rs` 配置。
- `shared/kiro-timeouts.js`:Kiro 共享超时常量与归一化工具,负责注册页/桌面授权链路复用的页面加载超时配置。 - `shared/kiro-timeouts.js`:Kiro 共享超时常量与归一化工具,负责注册页/桌面授权链路复用的页面加载超时配置。
- `core/flow-kernel/settings-schema.js`:统一设置 schema 与归一化层,负责把持久配置收敛到 `settingsState.services.*``settingsState.flows.*` 结构,并维护 `activeFlowId`、OpenAI 的 integration target、Kiro 的 `flows.kiro.targetId / targets``stepExecutionRangeByFlow` - `shared/contribution-registry.js`:贡献 adapter 与教程入口注册表,负责 OpenAI / Kiro 贡献 adapter、教程入口和发布贡献 flow 校验;默认发布校验只覆盖声明了 publication target 或支持贡献的 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 判定。 - `core/flow-kernel/source-registry.js`:运行时来源注册表,负责合并 flow runtime source 与共享 mail source,统一 source family、driver、URL 归属和 cleanup owner 判定。
## `sidepanel/` ## `sidepanel/`
@@ -207,7 +217,7 @@
receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱` receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱`
额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收 额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收
码或从 QQ / 网易 / Gmail 转发目标邮箱收码;当邮箱服务或邮箱生成器为 Cloud Mail 时,额外显示 API 地址、管理员账号、接收邮箱和生成域名配置行;当邮箱服务为 YYDS Mail 时,额外显示 API Key 与 Base URL 配置行,并隐藏普通邮箱生成/转发邮箱相关配置;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密 码或从 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/paypal-manager.js`:侧边栏 PayPal 账号管理器,负责 Plus 模式下的账号下拉框渲染、添加账号弹窗、保存账号与切换当前账号。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装 - `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装
配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 账号记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启 配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 账号记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启
@@ -221,7 +231,7 @@
会在 sidepanel 初始化时拉取并恢复到本地保存的国家/API 设置,接码开关关闭时会直接隐藏相关配置;账号记录快照同步改为默认自动模式,只 会在 sidepanel 初始化时拉取并恢复到本地保存的国家/API 设置,接码开关关闭时会直接隐藏相关配置;账号记录快照同步改为默认自动模式,只
要本地 helper 可用就会自动落盘;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑; 要本地 helper 可用就会自动落盘;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑;
Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;IP 代理配置保存、服务配置快照、开关应用和运行态回显由这里接入消息路由,具体解析/状态渲染下沉到 `sidepanel/ip-proxy-panel.js`;侧边栏初始化与点击“自动”前会刷新一次贡献站 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/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 代理配置。 - 补充:`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-relogin-wait.test.js`:测试 2925 强制重登入口 URL、长登录超时与前后固定等待窗口。
- `tests/background-mail2925-session-module.test.js`:测试 2925 会话模块已接入且导出工厂,并覆盖命中上限后“禁用 24 小时 + 切下一个号 + 自动登录”的核心链路。 - `tests/background-mail2925-session-module.test.js`:测试 2925 会话模块已接入且导出工厂,并覆盖命中上限后“禁用 24 小时 + 切下一个号 + 自动登录”的核心链路。
- `tests/background-mail2925-signup-flow.test.js`:测试注册页辅助层在 2925 provider 下,会先确保账号池里已分配可用账号,再继续生成别名邮箱。 - `tests/background-mail2925-signup-flow.test.js`:测试注册页辅助层在 2925 provider 下,会先确保账号池里已分配可用账号,再继续生成别名邮箱。
- `tests/background-mail-rule-registry-module.test.js`:测试邮件规则注册表与 OpenAI mail rules 模块接入,并覆盖验证码轮询 payload 构造 - `tests/background-flow-mail-polling-module.test.js`:测试 flow-aware 邮件轮询调度层对 API provider、网页邮箱 provider、2925 会话准备和 2925 上限恢复的共享分派行为
- `tests/background-mail-rule-registry-module.test.js`:测试邮件规则注册表与 OpenAI / Kiro mail rules 模块接入,并覆盖各 flow 验证码轮询 payload 构造。
- `tests/background-grok-state-module.test.js`:测试 Grok 运行态 helper 的默认值、兼容字段投影、节点完成回写、下游重置、fresh-attempt keep-state 与 SSO/upload 清理规则。
- `tests/background-grok-publisher-webchat2api.test.js`:测试 Grok `webchat2api` 上传器的地址归一化、必填配置校验、SSO payload、响应处理、运行态回写和敏感值日志保护。
- `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。 - `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。
- `tests/background-message-router-plus-final-step.test.js`:测试消息路由会按 Plus 当前最终节点追加成功记录,不再写死步骤 10。 - `tests/background-message-router-plus-final-step.test.js`:测试消息路由会按 Plus 当前最终节点追加成功记录,不再写死步骤 10。
- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑,并覆盖邮箱身份写入时清理旧手机号注册运行态。 - `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑,并覆盖邮箱身份写入时清理旧手机号注册运行态。
@@ -358,6 +371,7 @@
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。 - `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏账号记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。 - `tests/sidepanel-account-records-manager.test.js`:测试侧边栏账号记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
- `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。 - `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。
- `tests/contribution-registry.test.js`:测试贡献 adapter 注册、教程入口解析、默认发布贡献 flow 校验,以及 Grok 私有 `webchat2api` 上传 flow 不被自动纳入发布贡献检查。
- `tests/flow-capabilities-module.test.js`:测试 flow 能力矩阵模块对来源作用域、显隐能力和贡献模式边界的归一化。 - `tests/flow-capabilities-module.test.js`:测试 flow 能力矩阵模块对来源作用域、显隐能力和贡献模式边界的归一化。
- `tests/flow-registry-settings-schema.test.js`:测试 flow registry 与 settings schema 之间的默认值、来源映射和 flow-specific 配置结构保持一致。 - `tests/flow-registry-settings-schema.test.js`:测试 flow registry 与 settings schema 之间的默认值、来源映射和 flow-specific 配置结构保持一致。
- `tests/sidepanel-custom-email-pool.test.js`:测试侧边栏自定义邮箱池、自定义邮箱服务号池的 HTML 接线,以及邮箱池长度与自动轮数之间的联动规则。 - `tests/sidepanel-custom-email-pool.test.js`:测试侧边栏自定义邮箱池、自定义邮箱服务号池的 HTML 接线,以及邮箱池长度与自动轮数之间的联动规则。
@@ -365,6 +379,7 @@
- `tests/sidepanel-contribution-mode-flow-scope.test.js`:测试贡献模式只在 `openai` flow 内生效,不会污染 Kiro flow 的来源与面板作用域。 - `tests/sidepanel-contribution-mode-flow-scope.test.js`:测试贡献模式只在 `openai` flow 内生效,不会污染 Kiro flow 的来源与面板作用域。
- `tests/sidepanel-contribution-update-hint.test.js`:测试顶部贡献更新轻提示在公告、教程和托管 auto-run 提醒并存时的文案合成。 - `tests/sidepanel-contribution-update-hint.test.js`:测试顶部贡献更新轻提示在公告、教程和托管 auto-run 提醒并存时的文案合成。
- `tests/sidepanel-flow-source-registry.test.js`:测试 sidepanel 的 flow/source 选择器、Kiro 来源切换与显隐分组接线。 - `tests/sidepanel-flow-source-registry.test.js`:测试 sidepanel 的 flow/source 选择器、Kiro 来源切换与显隐分组接线。
- `tests/grok-runner.test.js`:测试 Grok 注册页执行器对邮箱提交、验证码轮询、资料提交、SSO Cookie 提取、账号记录收尾和完成状态持久化的关键链路。
- `tests/sidepanel-auto-run-content-refresh.test.js`:测试点击“自动”时会先冻结当前轮数并刷新贡献内容更新摘要,且刷新失败或启动前状态回灌不会阻塞或改写自动流程启动目标。 - `tests/sidepanel-auto-run-content-refresh.test.js`:测试点击“自动”时会先冻结当前轮数并刷新贡献内容更新摘要,且刷新失败或启动前状态回灌不会阻塞或改写自动流程启动目标。
- `tests/sidepanel-auto-run-risk-warning.test.js`:测试自动运行在高轮数且未配置节点轮询时会弹出风险提醒。 - `tests/sidepanel-auto-run-risk-warning.test.js`:测试自动运行在高轮数且未配置节点轮询时会弹出风险提醒。
- `tests/sidepanel-cloudflare-temp-email-random-subdomain.test.js`:测试 Cloudflare Temp Email 独立区域、随机子域名开关、域名列表恢复与提示文案联动。 - `tests/sidepanel-cloudflare-temp-email-random-subdomain.test.js`:测试 Cloudflare Temp Email 独立区域、随机子域名开关、域名列表恢复与提示文案联动。