merge: sync latest dev into sub2api branch
This commit is contained in:
+527
-25
@@ -11,6 +11,9 @@ const STEP7_RESTART_MAX_ROUNDS = 8;
|
||||
const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts';
|
||||
const DEFAULT_SUB2API_GROUP_NAME = 'codex';
|
||||
const DEFAULT_SUB2API_REDIRECT_URI = 'http://localhost:1455/auth/callback';
|
||||
const AUTO_RUN_ALARM_NAME = 'scheduled-auto-run';
|
||||
const AUTO_RUN_DELAY_MIN_MINUTES = 1;
|
||||
const AUTO_RUN_DELAY_MAX_MINUTES = 1440;
|
||||
|
||||
initializeSessionStorageAccess();
|
||||
|
||||
@@ -28,6 +31,8 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME, // SUB2API 创建账号时绑定的分组名。
|
||||
customPassword: '', // 自定义账号密码;留空时由程序自动生成随机密码。
|
||||
autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
|
||||
autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
|
||||
autoRunDelayMinutes: 30, // 自动运行倒计时分钟数。
|
||||
mailProvider: '163', // 验证码邮箱来源,当前支持 163 / inbucket。
|
||||
inbucketHost: '', // 仅当 mailProvider 为 inbucket 时填写 Inbucket 地址,其他情况保持为空。
|
||||
inbucketMailbox: '', // 仅当 mailProvider 为 inbucket 时填写邮箱名,其他情况保持为空。
|
||||
@@ -63,14 +68,49 @@ const DEFAULT_STATE = {
|
||||
autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。
|
||||
autoRunTotalRuns: 1, // 自动运行计划总轮数。
|
||||
autoRunAttemptRun: 0, // 当前轮次的重试序号。
|
||||
scheduledAutoRunAt: null, // 自动运行计划启动时间戳。
|
||||
scheduledAutoRunPlan: null, // 自动运行计划参数快照。
|
||||
};
|
||||
|
||||
function normalizeAutoRunDelayMinutes(value) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return PERSISTED_SETTING_DEFAULTS.autoRunDelayMinutes;
|
||||
}
|
||||
return Math.min(
|
||||
AUTO_RUN_DELAY_MAX_MINUTES,
|
||||
Math.max(AUTO_RUN_DELAY_MIN_MINUTES, Math.floor(numeric))
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeRunCount(value) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return 1;
|
||||
}
|
||||
return Math.min(50, Math.max(1, Math.floor(numeric)));
|
||||
}
|
||||
|
||||
function normalizeScheduledAutoRunPlan(plan) {
|
||||
if (!plan || typeof plan !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
totalRuns: normalizeRunCount(plan.totalRuns),
|
||||
autoRunSkipFailures: Boolean(plan.autoRunSkipFailures),
|
||||
mode: plan.mode === 'continue' ? 'continue' : 'restart',
|
||||
};
|
||||
}
|
||||
|
||||
async function getPersistedSettings() {
|
||||
const stored = await chrome.storage.local.get(PERSISTED_SETTING_KEYS);
|
||||
return {
|
||||
...PERSISTED_SETTING_DEFAULTS,
|
||||
...stored,
|
||||
autoRunSkipFailures: Boolean(stored.autoRunSkipFailures ?? PERSISTED_SETTING_DEFAULTS.autoRunSkipFailures),
|
||||
autoRunDelayEnabled: Boolean(stored.autoRunDelayEnabled ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayEnabled),
|
||||
autoRunDelayMinutes: normalizeAutoRunDelayMinutes(stored.autoRunDelayMinutes ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayMinutes),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,9 +144,13 @@ async function setPersistentSettings(updates) {
|
||||
const persistedUpdates = {};
|
||||
for (const key of PERSISTED_SETTING_KEYS) {
|
||||
if (updates[key] !== undefined) {
|
||||
persistedUpdates[key] = key === 'autoRunSkipFailures'
|
||||
? Boolean(updates[key])
|
||||
: updates[key];
|
||||
if (key === 'autoRunSkipFailures' || key === 'autoRunDelayEnabled') {
|
||||
persistedUpdates[key] = Boolean(updates[key]);
|
||||
} else if (key === 'autoRunDelayMinutes') {
|
||||
persistedUpdates[key] = normalizeAutoRunDelayMinutes(updates[key]);
|
||||
} else {
|
||||
persistedUpdates[key] = updates[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1006,6 +1050,56 @@ function isVerificationMailPollingError(error) {
|
||||
return /未在 .*邮箱中找到新的匹配邮件|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|did not respond in \d+s/i.test(message);
|
||||
}
|
||||
|
||||
const STEP7_RESTART_FROM_STEP6_ERROR_CODE = 'STEP7_RESTART_FROM_STEP6';
|
||||
const STEP7_RESTART_FROM_STEP6_MARKER_PATTERN = /^STEP7_RESTART_FROM_STEP6::([^:]+)::(.*)$/;
|
||||
|
||||
function createStep7RestartFromStep6Error(details = {}) {
|
||||
const { reason = 'unknown', url = '' } = details || {};
|
||||
const reasonLabel = reason === 'login_timeout_error_page'
|
||||
? '检测到登录页超时报错'
|
||||
: '步骤 7 请求回到步骤 6';
|
||||
const error = new Error(`步骤 7:${reasonLabel}。${url ? `URL: ${url}` : ''}`.trim());
|
||||
error.code = STEP7_RESTART_FROM_STEP6_ERROR_CODE;
|
||||
error.restartReason = reason;
|
||||
error.restartUrl = url;
|
||||
return error;
|
||||
}
|
||||
|
||||
function parseStep7RestartFromStep6Marker(message) {
|
||||
const normalized = getErrorMessage(message);
|
||||
const match = normalized.match(STEP7_RESTART_FROM_STEP6_MARKER_PATTERN);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
reason: match[1] || 'unknown',
|
||||
url: match[2] || '',
|
||||
};
|
||||
}
|
||||
|
||||
function getStep7RestartFromStep6Error(result) {
|
||||
if (result?.restartFromStep6) {
|
||||
return createStep7RestartFromStep6Error(result);
|
||||
}
|
||||
|
||||
const parsed = parseStep7RestartFromStep6Marker(result?.error);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createStep7RestartFromStep6Error(parsed);
|
||||
}
|
||||
|
||||
function isStep7RestartFromStep6Error(error) {
|
||||
return error?.code === STEP7_RESTART_FROM_STEP6_ERROR_CODE
|
||||
|| Boolean(parseStep7RestartFromStep6Marker(error));
|
||||
}
|
||||
|
||||
function isStep7RecoverableError(error) {
|
||||
return isVerificationMailPollingError(error) || isStep7RestartFromStep6Error(error);
|
||||
}
|
||||
|
||||
function isRestartCurrentAttemptError(error) {
|
||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||
return /当前邮箱已存在,需要重新开始新一轮/.test(message);
|
||||
@@ -1119,7 +1213,11 @@ function getAutoRunStatusPayload(phase, payload = {}) {
|
||||
const currentRun = payload.currentRun ?? autoRunCurrentRun;
|
||||
const totalRuns = payload.totalRuns ?? autoRunTotalRuns;
|
||||
const attemptRun = payload.attemptRun ?? autoRunAttemptRun;
|
||||
const autoRunning = phase === 'running' || phase === 'waiting_email' || phase === 'retrying';
|
||||
const rawScheduledAt = phase === 'scheduled'
|
||||
? (payload.scheduledAt ?? payload.scheduledAutoRunAt ?? null)
|
||||
: null;
|
||||
const scheduledAt = rawScheduledAt === null ? null : Number(rawScheduledAt);
|
||||
const autoRunning = phase === 'scheduled' || phase === 'running' || phase === 'waiting_email' || phase === 'retrying';
|
||||
|
||||
return {
|
||||
autoRunning,
|
||||
@@ -1127,18 +1225,26 @@ function getAutoRunStatusPayload(phase, payload = {}) {
|
||||
autoRunCurrentRun: currentRun,
|
||||
autoRunTotalRuns: totalRuns,
|
||||
autoRunAttemptRun: attemptRun,
|
||||
scheduledAutoRunAt: Number.isFinite(scheduledAt) ? scheduledAt : null,
|
||||
};
|
||||
}
|
||||
|
||||
async function broadcastAutoRunStatus(phase, payload = {}) {
|
||||
async function broadcastAutoRunStatus(phase, payload = {}, extraState = {}) {
|
||||
const rawScheduledAt = phase === 'scheduled'
|
||||
? (payload.scheduledAt ?? payload.scheduledAutoRunAt ?? null)
|
||||
: null;
|
||||
const statusPayload = {
|
||||
phase,
|
||||
currentRun: payload.currentRun ?? autoRunCurrentRun,
|
||||
totalRuns: payload.totalRuns ?? autoRunTotalRuns,
|
||||
attemptRun: payload.attemptRun ?? autoRunAttemptRun,
|
||||
scheduledAt: rawScheduledAt === null ? null : Number(rawScheduledAt),
|
||||
};
|
||||
|
||||
await setState(getAutoRunStatusPayload(phase, statusPayload));
|
||||
await setState({
|
||||
...extraState,
|
||||
...getAutoRunStatusPayload(phase, statusPayload),
|
||||
});
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'AUTO_RUN_STATUS',
|
||||
payload: statusPayload,
|
||||
@@ -1153,6 +1259,212 @@ function isAutoRunPausedState(state) {
|
||||
return Boolean(state.autoRunning) && state.autoRunPhase === 'waiting_email';
|
||||
}
|
||||
|
||||
function isAutoRunScheduledState(state) {
|
||||
const scheduledAt = state.scheduledAutoRunAt === null ? null : Number(state.scheduledAutoRunAt);
|
||||
return Boolean(state.autoRunning)
|
||||
&& state.autoRunPhase === 'scheduled'
|
||||
&& Number.isFinite(scheduledAt)
|
||||
&& Boolean(normalizeScheduledAutoRunPlan(state.scheduledAutoRunPlan));
|
||||
}
|
||||
|
||||
function formatAutoRunScheduleTime(timestamp) {
|
||||
return new Date(timestamp).toLocaleString('zh-CN', {
|
||||
hour12: false,
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
async function setAutoRunDelayEnabledState(enabled) {
|
||||
const normalized = Boolean(enabled);
|
||||
await setPersistentSettings({ autoRunDelayEnabled: normalized });
|
||||
await setState({ autoRunDelayEnabled: normalized });
|
||||
broadcastDataUpdate({ autoRunDelayEnabled: normalized });
|
||||
}
|
||||
|
||||
async function ensureScheduledAutoRunAlarm(scheduledAt) {
|
||||
if (!Number.isFinite(scheduledAt) || scheduledAt <= Date.now()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existingAlarm = await chrome.alarms.get(AUTO_RUN_ALARM_NAME);
|
||||
if (!existingAlarm || Math.abs((existingAlarm.scheduledTime || 0) - scheduledAt) > 1000) {
|
||||
await chrome.alarms.clear(AUTO_RUN_ALARM_NAME);
|
||||
await chrome.alarms.create(AUTO_RUN_ALARM_NAME, { when: scheduledAt });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function clearScheduledAutoRunAlarm() {
|
||||
await chrome.alarms.clear(AUTO_RUN_ALARM_NAME);
|
||||
}
|
||||
|
||||
async function scheduleAutoRun(totalRuns, options = {}) {
|
||||
const state = await getState();
|
||||
if (isAutoRunLockedState(state) || isAutoRunPausedState(state) || autoRunActive) {
|
||||
throw new Error('自动运行已在进行中,请先停止后再重新计划。');
|
||||
}
|
||||
if (isAutoRunScheduledState(state)) {
|
||||
throw new Error('已有自动运行倒计时计划,请先取消或立即开始。');
|
||||
}
|
||||
|
||||
const delayMinutes = normalizeAutoRunDelayMinutes(options.delayMinutes);
|
||||
const plan = normalizeScheduledAutoRunPlan({
|
||||
totalRuns,
|
||||
autoRunSkipFailures: options.autoRunSkipFailures,
|
||||
mode: options.mode,
|
||||
});
|
||||
const scheduledAt = Date.now() + delayMinutes * 60 * 1000;
|
||||
|
||||
autoRunCurrentRun = 0;
|
||||
autoRunTotalRuns = plan.totalRuns;
|
||||
autoRunAttemptRun = 0;
|
||||
|
||||
await ensureScheduledAutoRunAlarm(scheduledAt);
|
||||
await broadcastAutoRunStatus(
|
||||
'scheduled',
|
||||
{
|
||||
currentRun: 0,
|
||||
totalRuns: plan.totalRuns,
|
||||
attemptRun: 0,
|
||||
scheduledAt,
|
||||
},
|
||||
{
|
||||
autoRunSkipFailures: plan.autoRunSkipFailures,
|
||||
scheduledAutoRunPlan: plan,
|
||||
}
|
||||
);
|
||||
await addLog(
|
||||
`自动运行已计划:${delayMinutes} 分钟后启动(${formatAutoRunScheduleTime(scheduledAt)}),目标 ${plan.totalRuns} 轮。`,
|
||||
'info'
|
||||
);
|
||||
return { ok: true, scheduledAt };
|
||||
}
|
||||
|
||||
let scheduledAutoRunLaunching = false;
|
||||
|
||||
async function launchScheduledAutoRun(trigger = 'alarm') {
|
||||
if (scheduledAutoRunLaunching) {
|
||||
return false;
|
||||
}
|
||||
|
||||
scheduledAutoRunLaunching = true;
|
||||
try {
|
||||
const state = await getState();
|
||||
if (!isAutoRunScheduledState(state)) {
|
||||
return false;
|
||||
}
|
||||
if (autoRunActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const plan = normalizeScheduledAutoRunPlan(state.scheduledAutoRunPlan);
|
||||
if (!plan) {
|
||||
await clearScheduledAutoRunAlarm();
|
||||
await broadcastAutoRunStatus('idle', {
|
||||
currentRun: 0,
|
||||
totalRuns: 1,
|
||||
attemptRun: 0,
|
||||
}, {
|
||||
scheduledAutoRunPlan: null,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
await clearScheduledAutoRunAlarm();
|
||||
if (trigger !== 'manual' && state.autoRunDelayEnabled) {
|
||||
await setAutoRunDelayEnabledState(false);
|
||||
}
|
||||
await broadcastAutoRunStatus(
|
||||
'running',
|
||||
{
|
||||
currentRun: 0,
|
||||
totalRuns: plan.totalRuns,
|
||||
attemptRun: 0,
|
||||
},
|
||||
{
|
||||
autoRunSkipFailures: plan.autoRunSkipFailures,
|
||||
scheduledAutoRunPlan: null,
|
||||
}
|
||||
);
|
||||
|
||||
clearStopRequest();
|
||||
await addLog(
|
||||
trigger === 'manual'
|
||||
? '已手动跳过倒计时,自动运行立即开始。'
|
||||
: '倒计时结束,自动运行开始执行。',
|
||||
'info'
|
||||
);
|
||||
autoRunLoop(plan.totalRuns, {
|
||||
autoRunSkipFailures: plan.autoRunSkipFailures,
|
||||
mode: plan.mode,
|
||||
});
|
||||
return true;
|
||||
} finally {
|
||||
scheduledAutoRunLaunching = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelScheduledAutoRun(options = {}) {
|
||||
const state = await getState();
|
||||
if (!isAutoRunScheduledState(state)) {
|
||||
return false;
|
||||
}
|
||||
const plan = normalizeScheduledAutoRunPlan(state.scheduledAutoRunPlan);
|
||||
|
||||
await clearScheduledAutoRunAlarm();
|
||||
autoRunCurrentRun = 0;
|
||||
autoRunTotalRuns = plan?.totalRuns || 1;
|
||||
autoRunAttemptRun = 0;
|
||||
await broadcastAutoRunStatus(
|
||||
'idle',
|
||||
{
|
||||
currentRun: 0,
|
||||
totalRuns: plan?.totalRuns || 1,
|
||||
attemptRun: 0,
|
||||
},
|
||||
{
|
||||
scheduledAutoRunPlan: null,
|
||||
}
|
||||
);
|
||||
if (options.logMessage !== false) {
|
||||
await addLog(options.logMessage || '已取消自动运行倒计时计划。', 'warn');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function restoreScheduledAutoRunIfNeeded() {
|
||||
const state = await getState();
|
||||
if (state.autoRunPhase !== 'scheduled') {
|
||||
return;
|
||||
}
|
||||
|
||||
const plan = normalizeScheduledAutoRunPlan(state.scheduledAutoRunPlan);
|
||||
const scheduledAt = state.scheduledAutoRunAt === null ? null : Number(state.scheduledAutoRunAt);
|
||||
if (!plan || !Number.isFinite(scheduledAt)) {
|
||||
await clearScheduledAutoRunAlarm();
|
||||
await broadcastAutoRunStatus('idle', {
|
||||
currentRun: 0,
|
||||
totalRuns: 1,
|
||||
attemptRun: 0,
|
||||
}, {
|
||||
scheduledAutoRunPlan: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (scheduledAt <= Date.now()) {
|
||||
await launchScheduledAutoRun('restore');
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureScheduledAutoRunAlarm(scheduledAt);
|
||||
}
|
||||
|
||||
async function ensureManualInteractionAllowed(actionLabel) {
|
||||
const state = await getState();
|
||||
|
||||
@@ -1162,6 +1474,9 @@ async function ensureManualInteractionAllowed(actionLabel) {
|
||||
if (isAutoRunPausedState(state)) {
|
||||
throw new Error(`自动流程当前已暂停。请点击“继续”,或先确认接管自动流程后再${actionLabel}。`);
|
||||
}
|
||||
if (isAutoRunScheduledState(state)) {
|
||||
throw new Error(`自动流程已计划启动。请先取消计划,或立即开始后再${actionLabel}。`);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -1358,6 +1673,7 @@ async function handleMessage(message, sender) {
|
||||
|
||||
case 'RESET': {
|
||||
clearStopRequest();
|
||||
await clearScheduledAutoRunAlarm();
|
||||
await resetState();
|
||||
await addLog('流程已重置', 'info');
|
||||
return { ok: true };
|
||||
@@ -1382,7 +1698,11 @@ async function handleMessage(message, sender) {
|
||||
|
||||
case 'AUTO_RUN': {
|
||||
clearStopRequest();
|
||||
const totalRuns = message.payload?.totalRuns || 1;
|
||||
const state = await getState();
|
||||
if (isAutoRunScheduledState(state)) {
|
||||
throw new Error('已有自动运行倒计时计划,请先取消或立即开始。');
|
||||
}
|
||||
const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1);
|
||||
const autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures);
|
||||
const mode = message.payload?.mode === 'continue' ? 'continue' : 'restart';
|
||||
await setState({ autoRunSkipFailures });
|
||||
@@ -1390,6 +1710,33 @@ async function handleMessage(message, sender) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'SCHEDULE_AUTO_RUN': {
|
||||
clearStopRequest();
|
||||
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();
|
||||
const started = await launchScheduledAutoRun('manual');
|
||||
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 'RESUME_AUTO_RUN': {
|
||||
clearStopRequest();
|
||||
if (message.payload.email) {
|
||||
@@ -1421,6 +1768,8 @@ async function handleMessage(message, sender) {
|
||||
if (message.payload.sub2apiGroupName !== undefined) updates.sub2apiGroupName = message.payload.sub2apiGroupName;
|
||||
if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
|
||||
if (message.payload.autoRunSkipFailures !== undefined) updates.autoRunSkipFailures = Boolean(message.payload.autoRunSkipFailures);
|
||||
if (message.payload.autoRunDelayEnabled !== undefined) updates.autoRunDelayEnabled = Boolean(message.payload.autoRunDelayEnabled);
|
||||
if (message.payload.autoRunDelayMinutes !== undefined) updates.autoRunDelayMinutes = normalizeAutoRunDelayMinutes(message.payload.autoRunDelayMinutes);
|
||||
if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
|
||||
if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
|
||||
if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
|
||||
@@ -1575,6 +1924,17 @@ async function markRunningStepsStopped() {
|
||||
|
||||
async function requestStop(options = {}) {
|
||||
const { logMessage = '已收到停止请求,正在取消当前操作...' } = options;
|
||||
const state = await getState();
|
||||
|
||||
if (isAutoRunScheduledState(state) && !autoRunActive) {
|
||||
await cancelScheduledAutoRun({
|
||||
logMessage: options.logMessage === false
|
||||
? false
|
||||
: (options.logMessage || '已取消自动运行倒计时计划。'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (stopRequested) return;
|
||||
|
||||
stopRequested = true;
|
||||
@@ -2350,6 +2710,13 @@ async function requestVerificationCodeResend(step) {
|
||||
payload: {},
|
||||
});
|
||||
|
||||
if (step === 7) {
|
||||
const restartError = getStep7RestartFromStep6Error(result);
|
||||
if (restartError) {
|
||||
throw restartError;
|
||||
}
|
||||
}
|
||||
|
||||
if (result && result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
@@ -2436,6 +2803,13 @@ async function submitVerificationCode(step, code) {
|
||||
payload: { code },
|
||||
});
|
||||
|
||||
if (step === 7) {
|
||||
const restartError = getStep7RestartFromStep6Error(result);
|
||||
if (restartError) {
|
||||
throw restartError;
|
||||
}
|
||||
}
|
||||
|
||||
if (result && result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
@@ -2459,6 +2833,9 @@ async function resolveVerificationStep(step, state, mail, options = {}) {
|
||||
await requestVerificationCodeResend(step);
|
||||
await addLog(`步骤 ${step}:已先请求一封新的${getVerificationCodeLabel(step)}验证码,再开始轮询邮箱。`, 'warn');
|
||||
} catch (err) {
|
||||
if (step === 7 && isStep7RestartFromStep6Error(err)) {
|
||||
throw err;
|
||||
}
|
||||
await addLog(`步骤 ${step}:首次重新获取验证码失败:${err.message},将继续使用当前时间窗口轮询。`, 'warn');
|
||||
}
|
||||
}
|
||||
@@ -2647,6 +3024,11 @@ async function runStep7Attempt(state) {
|
||||
payload: {},
|
||||
});
|
||||
|
||||
const restartError = getStep7RestartFromStep6Error(prepareResult);
|
||||
if (restartError) {
|
||||
throw restartError;
|
||||
}
|
||||
|
||||
if (prepareResult && prepareResult.error) {
|
||||
throw new Error(prepareResult.error);
|
||||
}
|
||||
@@ -2701,7 +3083,7 @@ async function executeStep7(state) {
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
|
||||
if (!isVerificationMailPollingError(err)) {
|
||||
if (!isStep7RecoverableError(err)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -2709,11 +3091,20 @@ async function executeStep7(state) {
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`步骤 7:检测到邮箱轮询类失败,准备从步骤 6 重新开始(${round + 1}/${STEP7_RESTART_MAX_ROUNDS})...`, 'warn');
|
||||
await addLog(
|
||||
isStep7RestartFromStep6Error(err)
|
||||
? `步骤 7:检测到登录页超时报错,准备从步骤 6 重新开始(${round + 1}/${STEP7_RESTART_MAX_ROUNDS})...`
|
||||
: `步骤 7:检测到邮箱轮询类失败,准备从步骤 6 重新开始(${round + 1}/${STEP7_RESTART_MAX_ROUNDS})...`,
|
||||
'warn'
|
||||
);
|
||||
await rerunStep6ForStep7Recovery();
|
||||
}
|
||||
}
|
||||
|
||||
if (lastError && isStep7RecoverableError(lastError)) {
|
||||
throw new Error(`步骤 7:登录验证码流程在 ${STEP7_RESTART_MAX_ROUNDS} 轮恢复后仍未成功。最后一次原因:${lastError.message}`);
|
||||
}
|
||||
|
||||
throw lastError || new Error(`步骤 7:登录验证码流程在 ${STEP7_RESTART_MAX_ROUNDS} 轮后仍未成功。`);
|
||||
}
|
||||
|
||||
@@ -2725,9 +3116,11 @@ let webNavListener = null;
|
||||
let webNavCommittedListener = null;
|
||||
let step8TabUpdatedListener = null;
|
||||
let step8PendingReject = null;
|
||||
const STEP8_CLICK_EFFECT_TIMEOUT_MS = 3500;
|
||||
const STEP8_CLICK_EFFECT_TIMEOUT_MS = 10000;
|
||||
const STEP8_CLICK_RETRY_DELAY_MS = 500;
|
||||
const STEP8_READY_WAIT_TIMEOUT_MS = 30000;
|
||||
const STEP8_MAX_ROUNDS = 5;
|
||||
const STEP8_SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/signup-page.js'];
|
||||
const STEP8_STRATEGIES = [
|
||||
{ mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
|
||||
{ mode: 'debugger', label: 'debugger click' },
|
||||
@@ -2764,6 +3157,16 @@ function throwIfStep8SettledOrStopped(isSettled = false) {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureStep8SignupPageReady(tabId, options = {}) {
|
||||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||||
inject: STEP8_SIGNUP_PAGE_INJECT_FILES,
|
||||
injectSource: 'signup-page',
|
||||
timeoutMs: options.timeoutMs ?? 15000,
|
||||
retryDelayMs: options.retryDelayMs ?? 600,
|
||||
logMessage: options.logMessage || '',
|
||||
});
|
||||
}
|
||||
|
||||
async function getStep8PageState(tabId, responseTimeoutMs = 1500) {
|
||||
try {
|
||||
const result = await sendTabMessageWithTimeout(tabId, 'signup-page', {
|
||||
@@ -2785,6 +3188,7 @@ async function getStep8PageState(tabId, responseTimeoutMs = 1500) {
|
||||
|
||||
async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) {
|
||||
const start = Date.now();
|
||||
let recovered = false;
|
||||
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
throwIfStopped();
|
||||
@@ -2795,13 +3199,26 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS)
|
||||
if (pageState?.consentReady) {
|
||||
return pageState;
|
||||
}
|
||||
if (pageState === null && !recovered) {
|
||||
recovered = true;
|
||||
await ensureStep8SignupPageReady(tabId, {
|
||||
timeoutMs: Math.min(10000, timeoutMs),
|
||||
logMessage: '步骤 8:认证页内容脚本已失联,正在等待页面重新就绪...',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
recovered = false;
|
||||
await sleepWithStop(250);
|
||||
}
|
||||
|
||||
throw new Error('步骤 8:长时间未进入 OAuth 同意页,无法定位“继续”按钮。');
|
||||
}
|
||||
|
||||
async function prepareStep8DebuggerClick() {
|
||||
async function prepareStep8DebuggerClick(tabId) {
|
||||
await ensureStep8SignupPageReady(tabId, {
|
||||
timeoutMs: 15000,
|
||||
logMessage: '步骤 8:认证页内容脚本已失联,正在恢复后继续定位按钮...',
|
||||
});
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'STEP8_FIND_AND_CLICK',
|
||||
source: 'background',
|
||||
@@ -2819,7 +3236,11 @@ async function prepareStep8DebuggerClick() {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function triggerStep8ContentStrategy(strategy) {
|
||||
async function triggerStep8ContentStrategy(tabId, strategy) {
|
||||
await ensureStep8SignupPageReady(tabId, {
|
||||
timeoutMs: 15000,
|
||||
logMessage: '步骤 8:认证页内容脚本已失联,正在恢复后继续点击“继续”按钮...',
|
||||
});
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'STEP8_TRIGGER_CONTINUE',
|
||||
source: 'background',
|
||||
@@ -2841,8 +3262,51 @@ async function triggerStep8ContentStrategy(strategy) {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function reloadStep8ConsentPage(tabId, timeoutMs = 30000) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
throw new Error('步骤 8:缺少有效的认证页标签页,无法刷新后重试。');
|
||||
}
|
||||
|
||||
await chrome.tabs.update(tabId, { active: true }).catch(() => { });
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
const timer = setTimeout(() => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
chrome.tabs.onUpdated.removeListener(listener);
|
||||
reject(new Error('步骤 8:刷新认证页后等待页面完成加载超时。'));
|
||||
}, timeoutMs);
|
||||
|
||||
const listener = (updatedTabId, changeInfo) => {
|
||||
if (updatedTabId !== tabId) return;
|
||||
if (changeInfo.status !== 'complete') return;
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
chrome.tabs.onUpdated.removeListener(listener);
|
||||
resolve();
|
||||
};
|
||||
|
||||
chrome.tabs.onUpdated.addListener(listener);
|
||||
chrome.tabs.reload(tabId, { bypassCache: false }).catch((err) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
chrome.tabs.onUpdated.removeListener(listener);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
await ensureStep8SignupPageReady(tabId, {
|
||||
timeoutMs: Math.min(15000, timeoutMs),
|
||||
logMessage: '步骤 8:认证页刷新后内容脚本尚未就绪,正在等待页面恢复...',
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLICK_EFFECT_TIMEOUT_MS) {
|
||||
const start = Date.now();
|
||||
let recovered = false;
|
||||
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
throwIfStopped();
|
||||
@@ -2861,11 +3325,18 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI
|
||||
throw new Error('步骤 8:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。');
|
||||
}
|
||||
if (pageState === null) {
|
||||
return { progressed: true, reason: 'page_reloading' };
|
||||
}
|
||||
if (pageState && !pageState.consentPage) {
|
||||
return { progressed: true, reason: 'left_consent_page', url: pageState.url };
|
||||
if (!recovered) {
|
||||
recovered = true;
|
||||
await ensureStep8SignupPageReady(tabId, {
|
||||
timeoutMs: Math.max(3000, Math.min(8000, timeoutMs)),
|
||||
logMessage: '步骤 8:点击后认证页正在重载,正在等待内容脚本重新就绪...',
|
||||
}).catch(() => null);
|
||||
continue;
|
||||
}
|
||||
await sleepWithStop(200);
|
||||
continue;
|
||||
}
|
||||
recovered = false;
|
||||
|
||||
await sleepWithStop(200);
|
||||
}
|
||||
@@ -2967,9 +3438,12 @@ async function executeStep8(state) {
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
|
||||
chrome.webNavigation.onCommitted.addListener(webNavCommittedListener);
|
||||
chrome.tabs.onUpdated.addListener(step8TabUpdatedListener);
|
||||
await ensureStep8SignupPageReady(signupTabId, {
|
||||
timeoutMs: 15000,
|
||||
logMessage: '步骤 8:认证页内容脚本尚未就绪,正在等待页面恢复...',
|
||||
});
|
||||
|
||||
let attempt = 0;
|
||||
while (!resolved) {
|
||||
for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) {
|
||||
throwIfStep8SettledOrStopped(resolved);
|
||||
const pageState = await waitForStep8Ready(signupTabId);
|
||||
if (!pageState?.consentReady) {
|
||||
@@ -2977,18 +3451,16 @@ async function executeStep8(state) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const strategy = STEP8_STRATEGIES[attempt % STEP8_STRATEGIES.length];
|
||||
const round = attempt + 1;
|
||||
attempt += 1;
|
||||
const strategy = STEP8_STRATEGIES[Math.min(round - 1, STEP8_STRATEGIES.length - 1)];
|
||||
|
||||
await addLog(`步骤 8:第 ${round} 次尝试点击“继续”(${strategy.label})...`);
|
||||
await addLog(`步骤 8:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`);
|
||||
|
||||
if (strategy.mode === 'debugger') {
|
||||
const clickTarget = await prepareStep8DebuggerClick();
|
||||
const clickTarget = await prepareStep8DebuggerClick(signupTabId);
|
||||
throwIfStep8SettledOrStopped(resolved);
|
||||
await clickWithDebugger(signupTabId, clickTarget?.rect);
|
||||
} else {
|
||||
await triggerStep8ContentStrategy(strategy.strategy);
|
||||
await triggerStep8ContentStrategy(signupTabId, strategy.strategy);
|
||||
}
|
||||
|
||||
if (resolved) {
|
||||
@@ -3005,7 +3477,12 @@ async function executeStep8(state) {
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`步骤 8:${strategy.label} 本次未触发页面离开同意页,准备继续重试。`, 'warn');
|
||||
if (round >= STEP8_MAX_ROUNDS) {
|
||||
throw new Error(`步骤 8:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`);
|
||||
}
|
||||
|
||||
await addLog(`步骤 8:${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn');
|
||||
await reloadStep8ConsentPage(signupTabId);
|
||||
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -3158,3 +3635,28 @@ async function executeSub2ApiStep9(state) {
|
||||
// ============================================================
|
||||
|
||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
||||
|
||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||
if (alarm.name !== AUTO_RUN_ALARM_NAME) {
|
||||
return;
|
||||
}
|
||||
launchScheduledAutoRun('alarm').catch((err) => {
|
||||
console.error(LOG_PREFIX, 'Failed to launch scheduled auto run from alarm:', err);
|
||||
});
|
||||
});
|
||||
|
||||
chrome.runtime.onStartup.addListener(() => {
|
||||
restoreScheduledAutoRunIfNeeded().catch((err) => {
|
||||
console.error(LOG_PREFIX, 'Failed to restore scheduled auto run on startup:', err);
|
||||
});
|
||||
});
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
restoreScheduledAutoRunIfNeeded().catch((err) => {
|
||||
console.error(LOG_PREFIX, 'Failed to restore scheduled auto run on install/update:', err);
|
||||
});
|
||||
});
|
||||
|
||||
restoreScheduledAutoRunIfNeeded().catch((err) => {
|
||||
console.error(LOG_PREFIX, 'Failed to restore scheduled auto run:', err);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user