Merge branch 'dev'
This commit is contained in:
@@ -215,6 +215,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
- 手动输入:使用你自定义的密码
|
||||
- 可通过输入框右侧的眼睛图标切换显示
|
||||
- 配置会自动保存,也可以点击右侧 `保存` 按钮手动保存一次
|
||||
- 右上角 `配置` 按钮支持导出当前配置到 JSON 文件,也支持从 JSON 文件覆盖导入配置
|
||||
|
||||
扩展会把本轮实际使用的密码同步回侧边栏,便于查看和复制。
|
||||
|
||||
@@ -369,6 +370,9 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
|
||||
- 步骤 9 会拒绝任何不是真实 `/auth/callback`,或缺少 `code` / `state` 的本地回调地址
|
||||
- 成功后的清理只会针对 `/auth` 这一类真实回调标签页,不会再泛化清理任意 localhost 路径
|
||||
- 侧边栏可切换“本地 CPA”策略,默认是 `全部回调`
|
||||
- 选择 `全部回调` 时,即使 CPA 部署在本地,也会执行步骤 9
|
||||
- 选择 `跳过第9步` 时,仅当本地 CPA 且步骤 8 已拿到回调地址时,才会直接跳过步骤 9
|
||||
|
||||
回到 CPA 面板:
|
||||
|
||||
|
||||
+259
-53
@@ -39,6 +39,7 @@ 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;
|
||||
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
|
||||
|
||||
initializeSessionStorageAccess();
|
||||
|
||||
@@ -50,6 +51,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
panelMode: 'cpa', // Step 1 / Step 9 的来源模式:cpa | sub2api。
|
||||
vpsUrl: '', // VPS 面板地址,可手动填写。
|
||||
vpsPassword: '', // VPS 面板登录密码,可手动填写。
|
||||
localCpaStep9Mode: DEFAULT_LOCAL_CPA_STEP9_MODE, // 本地 CPA 的第 9 步策略:submit | bypass。
|
||||
sub2apiUrl: DEFAULT_SUB2API_URL, // SUB2API 管理后台地址。
|
||||
sub2apiEmail: '', // SUB2API 登录邮箱。
|
||||
sub2apiPassword: '', // SUB2API 登录密码。
|
||||
@@ -68,6 +70,8 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
};
|
||||
|
||||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||||
const SETTINGS_EXPORT_SCHEMA_VERSION = 1;
|
||||
const SETTINGS_EXPORT_FILENAME_PREFIX = 'multipage-settings';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
currentStep: 0, // 当前流程执行到的步骤编号。
|
||||
@@ -139,6 +143,30 @@ function normalizeEmailGenerator(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'cloudflare' ? 'cloudflare' : 'duck';
|
||||
}
|
||||
|
||||
function normalizePanelMode(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'sub2api' ? 'sub2api' : 'cpa';
|
||||
}
|
||||
|
||||
function normalizeMailProvider(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
switch (normalized) {
|
||||
case HOTMAIL_PROVIDER:
|
||||
case '163':
|
||||
case '163-vip':
|
||||
case 'qq':
|
||||
case 'inbucket':
|
||||
return normalized;
|
||||
default:
|
||||
return PERSISTED_SETTING_DEFAULTS.mailProvider;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLocalCpaStep9Mode(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'bypass'
|
||||
? 'bypass'
|
||||
: DEFAULT_LOCAL_CPA_STEP9_MODE;
|
||||
}
|
||||
|
||||
function normalizeCloudflareDomain(rawValue = '') {
|
||||
let value = String(rawValue || '').trim().toLowerCase();
|
||||
if (!value) return '';
|
||||
@@ -149,17 +177,99 @@ function normalizeCloudflareDomain(rawValue = '') {
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeCloudflareDomains(values) {
|
||||
const normalizedDomains = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const value of Array.isArray(values) ? values : []) {
|
||||
const normalized = normalizeCloudflareDomain(value);
|
||||
if (!normalized || seen.has(normalized)) continue;
|
||||
seen.add(normalized);
|
||||
normalizedDomains.push(normalized);
|
||||
}
|
||||
|
||||
return normalizedDomains;
|
||||
}
|
||||
|
||||
function normalizePersistentSettingValue(key, value) {
|
||||
switch (key) {
|
||||
case 'panelMode':
|
||||
return normalizePanelMode(value);
|
||||
case 'vpsUrl':
|
||||
return String(value || '').trim();
|
||||
case 'vpsPassword':
|
||||
return String(value || '');
|
||||
case 'localCpaStep9Mode':
|
||||
return normalizeLocalCpaStep9Mode(value);
|
||||
case 'sub2apiUrl':
|
||||
return String(value || '').trim();
|
||||
case 'sub2apiEmail':
|
||||
return String(value || '').trim();
|
||||
case 'sub2apiPassword':
|
||||
return String(value || '');
|
||||
case 'sub2apiGroupName':
|
||||
return String(value || '').trim();
|
||||
case 'customPassword':
|
||||
return String(value || '');
|
||||
case 'autoRunSkipFailures':
|
||||
case 'autoRunDelayEnabled':
|
||||
return Boolean(value);
|
||||
case 'autoRunDelayMinutes':
|
||||
return normalizeAutoRunDelayMinutes(value);
|
||||
case 'mailProvider':
|
||||
return normalizeMailProvider(value);
|
||||
case 'emailGenerator':
|
||||
return normalizeEmailGenerator(value);
|
||||
case 'inbucketHost':
|
||||
return String(value || '').trim();
|
||||
case 'inbucketMailbox':
|
||||
return String(value || '').trim();
|
||||
case 'cloudflareDomain':
|
||||
return normalizeCloudflareDomain(value);
|
||||
case 'cloudflareDomains':
|
||||
return normalizeCloudflareDomains(value);
|
||||
case 'hotmailAccounts':
|
||||
return normalizeHotmailAccounts(value);
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function buildPersistentSettingsPayload(input = {}, options = {}) {
|
||||
const { fillDefaults = false, requireKnownKeys = false } = options;
|
||||
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
||||
throw new Error('\u914d\u7f6e\u5185\u5bb9\u683c\u5f0f\u65e0\u6548\u3002');
|
||||
}
|
||||
|
||||
const payload = {};
|
||||
let matchedKeyCount = 0;
|
||||
for (const key of PERSISTED_SETTING_KEYS) {
|
||||
if (input[key] !== undefined) {
|
||||
payload[key] = normalizePersistentSettingValue(key, input[key]);
|
||||
matchedKeyCount += 1;
|
||||
} else if (fillDefaults) {
|
||||
payload[key] = normalizePersistentSettingValue(key, PERSISTED_SETTING_DEFAULTS[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (requireKnownKeys && matchedKeyCount === 0) {
|
||||
throw new Error('\u914d\u7f6e\u6587\u4ef6\u4e2d\u6ca1\u6709\u53ef\u8bc6\u522b\u7684\u914d\u7f6e\u5185\u5bb9\u3002');
|
||||
}
|
||||
|
||||
if (payload.cloudflareDomains) {
|
||||
const domains = normalizeCloudflareDomains(payload.cloudflareDomains);
|
||||
if (payload.cloudflareDomain && !domains.includes(payload.cloudflareDomain)) {
|
||||
domains.unshift(payload.cloudflareDomain);
|
||||
}
|
||||
payload.cloudflareDomains = domains;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
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),
|
||||
emailGenerator: normalizeEmailGenerator(stored.emailGenerator ?? PERSISTED_SETTING_DEFAULTS.emailGenerator),
|
||||
hotmailAccounts: normalizeHotmailAccounts(stored.hotmailAccounts),
|
||||
};
|
||||
return buildPersistentSettingsPayload(stored, { fillDefaults: true });
|
||||
}
|
||||
|
||||
async function getState() {
|
||||
@@ -191,26 +301,75 @@ async function setState(updates) {
|
||||
}
|
||||
|
||||
async function setPersistentSettings(updates) {
|
||||
const persistedUpdates = {};
|
||||
for (const key of PERSISTED_SETTING_KEYS) {
|
||||
if (updates[key] !== undefined) {
|
||||
if (key === 'autoRunSkipFailures' || key === 'autoRunDelayEnabled') {
|
||||
persistedUpdates[key] = Boolean(updates[key]);
|
||||
} else if (key === 'autoRunDelayMinutes') {
|
||||
persistedUpdates[key] = normalizeAutoRunDelayMinutes(updates[key]);
|
||||
} else if (key === 'hotmailAccounts') {
|
||||
persistedUpdates[key] = normalizeHotmailAccounts(updates[key]);
|
||||
} else {
|
||||
persistedUpdates[key] = updates[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
const persistedUpdates = buildPersistentSettingsPayload(updates);
|
||||
|
||||
if (Object.keys(persistedUpdates).length > 0) {
|
||||
await chrome.storage.local.set(persistedUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
function buildSettingsExportFilename(date = new Date()) {
|
||||
const pad = (value) => String(value).padStart(2, '0');
|
||||
return `${SETTINGS_EXPORT_FILENAME_PREFIX}-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}.json`;
|
||||
}
|
||||
|
||||
async function exportSettingsBundle() {
|
||||
const settings = await getPersistedSettings();
|
||||
const bundle = {
|
||||
schemaVersion: SETTINGS_EXPORT_SCHEMA_VERSION,
|
||||
exportedAt: new Date().toISOString(),
|
||||
extensionVersion: chrome.runtime.getManifest().version,
|
||||
settings,
|
||||
};
|
||||
|
||||
return {
|
||||
fileName: buildSettingsExportFilename(),
|
||||
fileContent: JSON.stringify(bundle, null, 2),
|
||||
};
|
||||
}
|
||||
|
||||
async function importSettingsBundle(configBundle) {
|
||||
const state = await ensureManualInteractionAllowed('\u5bfc\u5165\u914d\u7f6e');
|
||||
if (Object.values(state.stepStatuses || {}).some((status) => status === 'running')) {
|
||||
throw new Error('\u5f53\u524d\u6709\u6b65\u9aa4\u6b63\u5728\u6267\u884c\uff0c\u65e0\u6cd5\u5bfc\u5165\u914d\u7f6e\u3002');
|
||||
}
|
||||
if (!configBundle || typeof configBundle !== 'object' || Array.isArray(configBundle)) {
|
||||
throw new Error('\u914d\u7f6e\u6587\u4ef6\u5185\u5bb9\u65e0\u6548\u3002');
|
||||
}
|
||||
|
||||
const schemaVersion = Number(configBundle.schemaVersion);
|
||||
if (schemaVersion !== SETTINGS_EXPORT_SCHEMA_VERSION) {
|
||||
throw new Error(`\u4ec5\u652f\u6301\u5bfc\u5165 schemaVersion=${SETTINGS_EXPORT_SCHEMA_VERSION} \u7684\u914d\u7f6e\u6587\u4ef6\u3002`);
|
||||
}
|
||||
if (!configBundle.settings || typeof configBundle.settings !== 'object' || Array.isArray(configBundle.settings)) {
|
||||
throw new Error('\u914d\u7f6e\u6587\u4ef6\u7f3a\u5c11 settings \u914d\u7f6e\u6bb5\u3002');
|
||||
}
|
||||
|
||||
const importedSettings = buildPersistentSettingsPayload(configBundle.settings, {
|
||||
fillDefaults: true,
|
||||
requireKnownKeys: true,
|
||||
});
|
||||
|
||||
await setPersistentSettings(importedSettings);
|
||||
|
||||
const sessionUpdates = {
|
||||
...importedSettings,
|
||||
currentHotmailAccountId: null,
|
||||
};
|
||||
if (importedSettings.mailProvider === HOTMAIL_PROVIDER) {
|
||||
sessionUpdates.email = null;
|
||||
}
|
||||
|
||||
await setState(sessionUpdates);
|
||||
broadcastDataUpdate({
|
||||
...importedSettings,
|
||||
currentHotmailAccountId: null,
|
||||
...(sessionUpdates.email !== undefined ? { email: sessionUpdates.email } : {}),
|
||||
});
|
||||
|
||||
return getState();
|
||||
}
|
||||
|
||||
function broadcastDataUpdate(payload) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'DATA_UPDATED',
|
||||
@@ -218,9 +377,13 @@ function broadcastDataUpdate(payload) {
|
||||
}).catch(() => { });
|
||||
}
|
||||
|
||||
async function setEmailState(email) {
|
||||
async function setEmailStateSilently(email) {
|
||||
await setState({ email });
|
||||
broadcastDataUpdate({ email });
|
||||
}
|
||||
|
||||
async function setEmailState(email) {
|
||||
await setEmailStateSilently(email);
|
||||
if (email) {
|
||||
await resumeAutoRunIfWaitingForEmail();
|
||||
}
|
||||
@@ -983,7 +1146,9 @@ function isLocalCpaUrl(rawUrl) {
|
||||
}
|
||||
|
||||
function shouldBypassStep9ForLocalCpa(state) {
|
||||
return Boolean(state?.localhostUrl) && isLocalCpaUrl(state?.vpsUrl);
|
||||
return normalizeLocalCpaStep9Mode(state?.localCpaStep9Mode) === 'bypass'
|
||||
&& Boolean(state?.localhostUrl)
|
||||
&& isLocalCpaUrl(state?.vpsUrl);
|
||||
}
|
||||
|
||||
function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
|
||||
@@ -1881,6 +2046,13 @@ function clearStopRequest() {
|
||||
stopRequested = false;
|
||||
}
|
||||
|
||||
function getRunningSteps(statuses = {}) {
|
||||
return Object.entries({ ...DEFAULT_STATE.stepStatuses, ...statuses })
|
||||
.filter(([, status]) => status === 'running')
|
||||
.map(([step]) => Number(step))
|
||||
.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function getAutoRunStatusPayload(phase, payload = {}) {
|
||||
const currentRun = payload.currentRun ?? autoRunCurrentRun;
|
||||
const totalRuns = payload.totalRuns ?? autoRunTotalRuns;
|
||||
@@ -1889,7 +2061,7 @@ function getAutoRunStatusPayload(phase, payload = {}) {
|
||||
? (payload.scheduledAt ?? payload.scheduledAutoRunAt ?? null)
|
||||
: null;
|
||||
const scheduledAt = rawScheduledAt === null ? null : Number(rawScheduledAt);
|
||||
const autoRunning = phase === 'scheduled' || phase === 'running' || phase === 'waiting_email' || phase === 'retrying';
|
||||
const autoRunning = phase === 'scheduled' || phase === 'running' || phase === 'waiting_step' || phase === 'waiting_email' || phase === 'retrying';
|
||||
|
||||
return {
|
||||
autoRunning,
|
||||
@@ -1924,7 +2096,7 @@ async function broadcastAutoRunStatus(phase, payload = {}, extraState = {}) {
|
||||
}
|
||||
|
||||
function isAutoRunLockedState(state) {
|
||||
return Boolean(state.autoRunning) && (state.autoRunPhase === 'running' || state.autoRunPhase === 'retrying');
|
||||
return Boolean(state.autoRunning) && (state.autoRunPhase === 'running' || state.autoRunPhase === 'waiting_step' || state.autoRunPhase === 'retrying');
|
||||
}
|
||||
|
||||
function isAutoRunPausedState(state) {
|
||||
@@ -2211,6 +2383,7 @@ async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY
|
||||
}
|
||||
|
||||
async function clickWithDebugger(tabId, rect) {
|
||||
throwIfStopped();
|
||||
if (!tabId) {
|
||||
throw new Error('未找到用于调试点击的认证页面标签页。');
|
||||
}
|
||||
@@ -2229,10 +2402,12 @@ async function clickWithDebugger(tabId, rect) {
|
||||
}
|
||||
|
||||
try {
|
||||
throwIfStopped();
|
||||
const x = Math.round(rect.centerX);
|
||||
const y = Math.round(rect.centerY);
|
||||
|
||||
await chrome.debugger.sendCommand(target, 'Page.bringToFront');
|
||||
throwIfStopped();
|
||||
await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
|
||||
type: 'mouseMoved',
|
||||
x,
|
||||
@@ -2241,6 +2416,7 @@ async function clickWithDebugger(tabId, rect) {
|
||||
buttons: 0,
|
||||
clickCount: 0,
|
||||
});
|
||||
throwIfStopped();
|
||||
await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
|
||||
type: 'mousePressed',
|
||||
x,
|
||||
@@ -2249,6 +2425,7 @@ async function clickWithDebugger(tabId, rect) {
|
||||
buttons: 1,
|
||||
clickCount: 1,
|
||||
});
|
||||
throwIfStopped();
|
||||
await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
|
||||
type: 'mouseReleased',
|
||||
x,
|
||||
@@ -2430,31 +2607,21 @@ async function handleMessage(message, sender) {
|
||||
}
|
||||
|
||||
case 'SAVE_SETTING': {
|
||||
const updates = {};
|
||||
if (message.payload.panelMode !== undefined) updates.panelMode = message.payload.panelMode;
|
||||
if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
|
||||
if (message.payload.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword;
|
||||
if (message.payload.sub2apiUrl !== undefined) updates.sub2apiUrl = message.payload.sub2apiUrl;
|
||||
if (message.payload.sub2apiEmail !== undefined) updates.sub2apiEmail = message.payload.sub2apiEmail;
|
||||
if (message.payload.sub2apiPassword !== undefined) updates.sub2apiPassword = message.payload.sub2apiPassword;
|
||||
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.emailGenerator !== undefined) updates.emailGenerator = normalizeEmailGenerator(message.payload.emailGenerator);
|
||||
if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
|
||||
if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
|
||||
if (message.payload.cloudflareDomain !== undefined) updates.cloudflareDomain = normalizeCloudflareDomain(message.payload.cloudflareDomain);
|
||||
if (message.payload.cloudflareDomains !== undefined) updates.cloudflareDomains = Array.isArray(message.payload.cloudflareDomains)
|
||||
? message.payload.cloudflareDomains.map(domain => normalizeCloudflareDomain(domain)).filter(Boolean)
|
||||
: [];
|
||||
const updates = buildPersistentSettingsPayload(message.payload || {});
|
||||
await setPersistentSettings(updates);
|
||||
await setState(updates);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'EXPORT_SETTINGS': {
|
||||
return { ok: true, ...(await exportSettingsBundle()) };
|
||||
}
|
||||
|
||||
case 'IMPORT_SETTINGS': {
|
||||
const state = await importSettingsBundle(message.payload?.config || null);
|
||||
return { ok: true, state };
|
||||
}
|
||||
|
||||
case 'UPSERT_HOTMAIL_ACCOUNT': {
|
||||
const account = await upsertHotmailAccount(message.payload || {});
|
||||
return { ok: true, account };
|
||||
@@ -2513,6 +2680,16 @@ async function handleMessage(message, sender) {
|
||||
}
|
||||
|
||||
// Side panel data updates
|
||||
case 'SET_EMAIL_STATE': {
|
||||
const state = await getState();
|
||||
if (isAutoRunLockedState(state)) {
|
||||
throw new Error('自动流程运行中,当前不能手动修改邮箱。');
|
||||
}
|
||||
const email = String(message.payload?.email || '').trim() || null;
|
||||
await setEmailStateSilently(email);
|
||||
return { ok: true, email };
|
||||
}
|
||||
|
||||
case 'SAVE_EMAIL': {
|
||||
const state = await getState();
|
||||
if (isAutoRunLockedState(state)) {
|
||||
@@ -2688,11 +2865,29 @@ async function completeStepFromBackground(step, payload = {}) {
|
||||
notifyStepComplete(step, payload);
|
||||
}
|
||||
|
||||
async function waitForRunningStepsToFinish(payload = {}) {
|
||||
let currentState = await getState();
|
||||
let runningSteps = getRunningSteps(currentState.stepStatuses);
|
||||
if (!runningSteps.length) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
await addLog(`自动继续:检测到步骤 ${runningSteps.join(', ')} 正在运行,等待完成后再继续自动流程...`, 'info');
|
||||
await broadcastAutoRunStatus('waiting_step', payload);
|
||||
|
||||
while (runningSteps.length) {
|
||||
await sleepWithStop(250);
|
||||
currentState = await getState();
|
||||
runningSteps = getRunningSteps(currentState.stepStatuses);
|
||||
}
|
||||
|
||||
await addLog('自动继续:当前运行步骤已结束,准备按最新进度继续自动流程...', 'info');
|
||||
return currentState;
|
||||
}
|
||||
|
||||
async function markRunningStepsStopped() {
|
||||
const state = await getState();
|
||||
const runningSteps = Object.entries(state.stepStatuses || {})
|
||||
.filter(([, status]) => status === 'running')
|
||||
.map(([step]) => Number(step));
|
||||
const runningSteps = getRunningSteps(state.stepStatuses);
|
||||
|
||||
for (const step of runningSteps) {
|
||||
await setStepStatus(step, 'stopped');
|
||||
@@ -3083,10 +3278,14 @@ async function autoRunLoop(totalRuns, options = {}) {
|
||||
let attemptRuns = Math.max(0, resumeAttemptRunsProcessed);
|
||||
let forceFreshTabsNextRun = false;
|
||||
let continueCurrentOnFirstAttempt = initialMode === 'continue';
|
||||
const initialState = await getState();
|
||||
const initialPhase = continueCurrentOnFirstAttempt && getRunningSteps(initialState.stepStatuses).length
|
||||
? 'waiting_step'
|
||||
: 'running';
|
||||
|
||||
await setState({
|
||||
autoRunSkipFailures,
|
||||
...getAutoRunStatusPayload('running', {
|
||||
...getAutoRunStatusPayload(initialPhase, {
|
||||
currentRun: resumeCurrentRun,
|
||||
totalRuns,
|
||||
attemptRun: resumeAttemptRunsProcessed,
|
||||
@@ -3102,7 +3301,14 @@ async function autoRunLoop(totalRuns, options = {}) {
|
||||
let useExistingProgress = false;
|
||||
|
||||
if (continueCurrentOnFirstAttempt) {
|
||||
const currentState = await getState();
|
||||
let currentState = await getState();
|
||||
if (getRunningSteps(currentState.stepStatuses).length) {
|
||||
currentState = await waitForRunningStepsToFinish({
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun: attemptRuns,
|
||||
});
|
||||
}
|
||||
const resumeStep = getFirstUnfinishedStep(currentState.stepStatuses);
|
||||
if (resumeStep && hasSavedProgress(currentState.stepStatuses)) {
|
||||
startStep = resumeStep;
|
||||
@@ -4415,7 +4621,7 @@ async function executeCpaStep9(state) {
|
||||
}
|
||||
|
||||
if (shouldBypassStep9ForLocalCpa(state)) {
|
||||
await addLog('步骤 9:检测到本地 CPA,步骤 8 完成后已自动添加,无需重复提交回调地址。', 'info');
|
||||
await addLog('步骤 9:检测到本地 CPA,且当前策略为“跳过第9步”,本轮不再重复提交回调地址。', 'info');
|
||||
await completeStepFromBackground(9, {
|
||||
localhostUrl: state.localhostUrl,
|
||||
verifiedStatus: 'local-auto',
|
||||
|
||||
@@ -142,6 +142,67 @@ header {
|
||||
[data-theme="dark"] .theme-toggle .icon-moon { display: none; }
|
||||
[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
|
||||
|
||||
.header-menu {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-config-btn {
|
||||
min-width: 0;
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
.header-config-btn[aria-expanded="true"] {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.header-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
min-width: 120px;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
background: var(--bg-base);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 1300;
|
||||
}
|
||||
|
||||
.header-dropdown[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.header-dropdown-item {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition), color var(--transition);
|
||||
}
|
||||
|
||||
.header-dropdown-item:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.header-dropdown-item:disabled {
|
||||
color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Buttons
|
||||
============================================================ */
|
||||
@@ -238,6 +299,20 @@ header {
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.data-card.is-locked {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-card.is-locked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background: color-mix(in srgb, var(--bg-base) 18%, transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.section-mini-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -283,6 +358,10 @@ header {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.data-value-actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.input-with-icon {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
@@ -307,6 +386,10 @@ header {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.data-value-fill {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.data-value.has-value { color: var(--text-primary); }
|
||||
|
||||
.mono {
|
||||
@@ -378,6 +461,41 @@ header {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.choice-group {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.choice-btn {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-base);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition), background var(--transition), color var(--transition), box-shadow var(--transition);
|
||||
}
|
||||
|
||||
.choice-btn:hover {
|
||||
border-color: var(--blue);
|
||||
color: var(--blue);
|
||||
background: var(--blue-soft);
|
||||
}
|
||||
|
||||
.choice-btn.is-active {
|
||||
border-color: var(--blue);
|
||||
color: var(--blue);
|
||||
background: var(--blue-soft);
|
||||
box-shadow: 0 0 0 1px var(--blue-glow);
|
||||
}
|
||||
|
||||
#btn-fetch-email,
|
||||
#btn-save-settings {
|
||||
padding-inline: 10px;
|
||||
|
||||
@@ -58,11 +58,19 @@
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||
</svg>
|
||||
</button>
|
||||
<div id="config-menu-shell" class="header-menu">
|
||||
<button id="btn-config-menu" class="btn btn-ghost btn-sm header-config-btn" type="button" aria-haspopup="menu"
|
||||
aria-expanded="false">配置</button>
|
||||
<div id="config-menu" class="header-dropdown" role="menu" aria-labelledby="btn-config-menu" hidden>
|
||||
<button id="btn-export-settings" class="header-dropdown-item" type="button" role="menuitem">导出配置</button>
|
||||
<button id="btn-import-settings" class="header-dropdown-item" type="button" role="menuitem">导入配置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="data-section">
|
||||
<div class="data-card">
|
||||
<div id="settings-card" class="data-card">
|
||||
<div class="data-row">
|
||||
<span class="data-label">来源</span>
|
||||
<select id="select-panel-mode" class="data-select">
|
||||
@@ -81,7 +89,18 @@
|
||||
</div>
|
||||
<div class="data-row" id="row-vps-password">
|
||||
<span class="data-label">管理密钥</span>
|
||||
<input type="password" id="input-vps-password" class="data-input" placeholder="请输入 CPA 管理密钥" />
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-vps-password" class="data-input data-input-with-icon" placeholder="请输入 CPA 管理密钥" />
|
||||
<button id="btn-toggle-vps-password" class="input-icon-btn" type="button" aria-label="显示管理密钥"
|
||||
title="显示管理密钥"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-local-cpa-step9-mode">
|
||||
<span class="data-label">本地 CPA</span>
|
||||
<div id="local-cpa-step9-mode-group" class="choice-group" role="group" aria-label="本地 CPA 第 9 步策略">
|
||||
<button type="button" class="choice-btn" data-local-cpa-step9-mode="submit">全部回调</button>
|
||||
<button type="button" class="choice-btn" data-local-cpa-step9-mode="bypass">跳过第9步</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-sub2api-url" style="display:none;">
|
||||
<span class="data-label">SUB2API</span>
|
||||
@@ -101,14 +120,11 @@
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">codex密码</span>
|
||||
<div class="data-inline">
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-password" class="data-input data-input-with-icon"
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-password" class="data-input data-input-with-icon"
|
||||
placeholder="codex密码,留空则自动生成" />
|
||||
<button id="btn-toggle-password" class="input-icon-btn" type="button" aria-label="显示密码"
|
||||
title="显示密码"></button>
|
||||
</div>
|
||||
<button id="btn-save-settings" class="btn btn-outline btn-sm" type="button">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
@@ -179,7 +195,10 @@
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">回调</span>
|
||||
<span id="display-localhost-url" class="data-value mono truncate">等待中...</span>
|
||||
<div class="data-inline data-value-actions">
|
||||
<span id="display-localhost-url" class="data-value data-value-fill mono truncate">等待中...</span>
|
||||
<button id="btn-save-settings" class="btn btn-outline btn-sm" type="button">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="hotmail-section" class="data-card hotmail-card" style="display:none;">
|
||||
@@ -332,6 +351,7 @@
|
||||
</div>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
<input id="input-import-settings-file" type="file" accept=".json,application/json" hidden />
|
||||
<script src="../hotmail-utils.js"></script>
|
||||
<script src="sidepanel.js"></script>
|
||||
</body>
|
||||
|
||||
+398
-62
@@ -11,6 +11,7 @@ const STATUS_ICONS = {
|
||||
};
|
||||
|
||||
const logArea = document.getElementById('log-area');
|
||||
const settingsCard = document.getElementById('settings-card');
|
||||
const displayOauthUrl = document.getElementById('display-oauth-url');
|
||||
const displayLocalhostUrl = document.getElementById('display-localhost-url');
|
||||
const displayStatus = document.getElementById('display-status');
|
||||
@@ -18,6 +19,7 @@ const statusBar = document.getElementById('status-bar');
|
||||
const inputEmail = document.getElementById('input-email');
|
||||
const inputPassword = document.getElementById('input-password');
|
||||
const btnToggleVpsUrl = document.getElementById('btn-toggle-vps-url');
|
||||
const btnToggleVpsPassword = document.getElementById('btn-toggle-vps-password');
|
||||
const btnFetchEmail = document.getElementById('btn-fetch-email');
|
||||
const btnTogglePassword = document.getElementById('btn-toggle-password');
|
||||
const btnSaveSettings = document.getElementById('btn-save-settings');
|
||||
@@ -33,11 +35,19 @@ const autoScheduleMeta = document.getElementById('auto-schedule-meta');
|
||||
const btnAutoRunNow = document.getElementById('btn-auto-run-now');
|
||||
const btnAutoCancelSchedule = document.getElementById('btn-auto-cancel-schedule');
|
||||
const btnClearLog = document.getElementById('btn-clear-log');
|
||||
const configMenuShell = document.getElementById('config-menu-shell');
|
||||
const btnConfigMenu = document.getElementById('btn-config-menu');
|
||||
const configMenu = document.getElementById('config-menu');
|
||||
const btnExportSettings = document.getElementById('btn-export-settings');
|
||||
const btnImportSettings = document.getElementById('btn-import-settings');
|
||||
const inputImportSettingsFile = document.getElementById('input-import-settings-file');
|
||||
const selectPanelMode = document.getElementById('select-panel-mode');
|
||||
const rowVpsUrl = document.getElementById('row-vps-url');
|
||||
const inputVpsUrl = document.getElementById('input-vps-url');
|
||||
const rowVpsPassword = document.getElementById('row-vps-password');
|
||||
const inputVpsPassword = document.getElementById('input-vps-password');
|
||||
const rowLocalCpaStep9Mode = document.getElementById('row-local-cpa-step9-mode');
|
||||
const localCpaStep9ModeButtons = Array.from(document.querySelectorAll('[data-local-cpa-step9-mode]'));
|
||||
const rowSub2ApiUrl = document.getElementById('row-sub2api-url');
|
||||
const inputSub2ApiUrl = document.getElementById('input-sub2api-url');
|
||||
const rowSub2ApiEmail = document.getElementById('row-sub2api-email');
|
||||
@@ -96,6 +106,7 @@ const SKIPPABLE_STEPS = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
const AUTO_DELAY_MIN_MINUTES = 1;
|
||||
const AUTO_DELAY_MAX_MINUTES = 1440;
|
||||
const AUTO_DELAY_DEFAULT_MINUTES = 30;
|
||||
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
|
||||
|
||||
let latestState = null;
|
||||
let currentAutoRun = {
|
||||
@@ -115,6 +126,8 @@ let currentModalActions = [];
|
||||
let scheduledCountdownTimer = null;
|
||||
let hotmailActionInFlight = false;
|
||||
let hotmailListExpanded = false;
|
||||
let configMenuOpen = false;
|
||||
let configActionInFlight = false;
|
||||
|
||||
const EYE_OPEN_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
|
||||
const EYE_CLOSED_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-6.94"/><path d="M9.9 4.24A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.86 21.86 0 0 1-2.16 3.19"/><path d="M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
|
||||
@@ -225,10 +238,14 @@ function openActionModal({ title, message, actions }) {
|
||||
});
|
||||
}
|
||||
|
||||
function openAutoStartChoiceDialog(startStep) {
|
||||
function openAutoStartChoiceDialog(startStep, options = {}) {
|
||||
const runningStep = Number.isInteger(options.runningStep) ? options.runningStep : null;
|
||||
const continueMessage = runningStep
|
||||
? `继续当前会先等待步骤 ${runningStep} 完成,再按最新进度自动执行。`
|
||||
: `继续当前会从步骤 ${startStep} 开始自动执行。`;
|
||||
return openActionModal({
|
||||
title: '启动自动',
|
||||
message: `检测到当前已有流程进度。继续当前会从步骤 ${startStep} 开始自动执行,重新开始会清空当前流程进度并从步骤 1 新开一轮。`,
|
||||
message: `检测到当前已有流程进度。${continueMessage}重新开始会清空当前流程进度并从步骤 1 新开一轮。`,
|
||||
actions: [
|
||||
{ id: null, label: '取消', variant: 'btn-ghost' },
|
||||
{ id: 'restart', label: '重新开始', variant: 'btn-outline' },
|
||||
@@ -249,6 +266,71 @@ async function openConfirmModal({ title, message, confirmLabel = '确认', confi
|
||||
return choice === 'confirm';
|
||||
}
|
||||
|
||||
function updateConfigMenuControls() {
|
||||
const disabled = configActionInFlight || settingsSaveInFlight;
|
||||
const importLocked = disabled
|
||||
|| currentAutoRun.autoRunning
|
||||
|| Object.values(getStepStatuses()).some((status) => status === 'running');
|
||||
if (btnConfigMenu) {
|
||||
btnConfigMenu.disabled = disabled;
|
||||
btnConfigMenu.setAttribute('aria-expanded', String(configMenuOpen));
|
||||
}
|
||||
if (configMenu) {
|
||||
configMenu.hidden = !configMenuOpen;
|
||||
}
|
||||
if (btnExportSettings) {
|
||||
btnExportSettings.disabled = disabled;
|
||||
}
|
||||
if (btnImportSettings) {
|
||||
btnImportSettings.disabled = importLocked;
|
||||
}
|
||||
}
|
||||
|
||||
function closeConfigMenu() {
|
||||
configMenuOpen = false;
|
||||
updateConfigMenuControls();
|
||||
}
|
||||
|
||||
function openConfigMenu() {
|
||||
configMenuOpen = true;
|
||||
updateConfigMenuControls();
|
||||
}
|
||||
|
||||
function toggleConfigMenu() {
|
||||
configMenuOpen ? closeConfigMenu() : openConfigMenu();
|
||||
}
|
||||
|
||||
async function waitForSettingsSaveIdle() {
|
||||
while (settingsSaveInFlight) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
}
|
||||
|
||||
async function flushPendingSettingsBeforeExport() {
|
||||
clearTimeout(settingsAutoSaveTimer);
|
||||
await waitForSettingsSaveIdle();
|
||||
if (settingsDirty) {
|
||||
await saveSettings({ silent: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function settlePendingSettingsBeforeImport() {
|
||||
clearTimeout(settingsAutoSaveTimer);
|
||||
await waitForSettingsSaveIdle();
|
||||
}
|
||||
|
||||
function downloadTextFile(content, fileName, mimeType = 'application/json;charset=utf-8') {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = objectUrl;
|
||||
anchor.download = fileName;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
|
||||
}
|
||||
|
||||
function isDoneStatus(status) {
|
||||
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
|
||||
}
|
||||
@@ -267,6 +349,14 @@ function getFirstUnfinishedStep(state = latestState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getRunningSteps(state = latestState) {
|
||||
const statuses = getStepStatuses(state);
|
||||
return Object.entries(statuses)
|
||||
.filter(([, status]) => status === 'running')
|
||||
.map(([step]) => Number(step))
|
||||
.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function hasSavedProgress(state = latestState) {
|
||||
const statuses = getStepStatuses(state);
|
||||
return Object.values(statuses).some((status) => status !== 'pending');
|
||||
@@ -293,7 +383,7 @@ function syncAutoRunState(source = {}) {
|
||||
const autoRunning = source.autoRunning !== undefined
|
||||
? Boolean(source.autoRunning)
|
||||
: (source.autoRunPhase !== undefined || source.phase !== undefined
|
||||
? ['scheduled', 'running', 'waiting_email', 'retrying'].includes(phase)
|
||||
? ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying'].includes(phase)
|
||||
: currentAutoRun.autoRunning);
|
||||
|
||||
currentAutoRun = {
|
||||
@@ -307,13 +397,17 @@ function syncAutoRunState(source = {}) {
|
||||
}
|
||||
|
||||
function isAutoRunLockedPhase() {
|
||||
return currentAutoRun.phase === 'running' || currentAutoRun.phase === 'retrying';
|
||||
return currentAutoRun.phase === 'running' || currentAutoRun.phase === 'waiting_step' || currentAutoRun.phase === 'retrying';
|
||||
}
|
||||
|
||||
function isAutoRunPausedPhase() {
|
||||
return currentAutoRun.phase === 'waiting_email';
|
||||
}
|
||||
|
||||
function isAutoRunWaitingStepPhase() {
|
||||
return currentAutoRun.phase === 'waiting_step';
|
||||
}
|
||||
|
||||
function isAutoRunScheduledPhase() {
|
||||
return currentAutoRun.phase === 'scheduled';
|
||||
}
|
||||
@@ -492,6 +586,7 @@ function collectSettingsPayload() {
|
||||
panelMode: selectPanelMode.value,
|
||||
vpsUrl: inputVpsUrl.value.trim(),
|
||||
vpsPassword: inputVpsPassword.value,
|
||||
localCpaStep9Mode: getSelectedLocalCpaStep9Mode(),
|
||||
sub2apiUrl: inputSub2ApiUrl.value.trim(),
|
||||
sub2apiEmail: inputSub2ApiEmail.value.trim(),
|
||||
sub2apiPassword: inputSub2ApiPassword.value,
|
||||
@@ -509,6 +604,68 @@ function collectSettingsPayload() {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLocalCpaStep9Mode(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'bypass'
|
||||
? 'bypass'
|
||||
: DEFAULT_LOCAL_CPA_STEP9_MODE;
|
||||
}
|
||||
|
||||
function getSelectedLocalCpaStep9Mode() {
|
||||
const activeButton = localCpaStep9ModeButtons.find((button) => button.classList.contains('is-active'));
|
||||
return normalizeLocalCpaStep9Mode(activeButton?.dataset.localCpaStep9Mode);
|
||||
}
|
||||
|
||||
function setLocalCpaStep9Mode(mode) {
|
||||
const resolvedMode = normalizeLocalCpaStep9Mode(mode);
|
||||
localCpaStep9ModeButtons.forEach((button) => {
|
||||
const active = button.dataset.localCpaStep9Mode === resolvedMode;
|
||||
button.classList.toggle('is-active', active);
|
||||
button.setAttribute('aria-pressed', String(active));
|
||||
});
|
||||
}
|
||||
|
||||
function setSettingsCardLocked(locked) {
|
||||
if (!settingsCard) {
|
||||
return;
|
||||
}
|
||||
settingsCard.classList.toggle('is-locked', locked);
|
||||
settingsCard.toggleAttribute('inert', locked);
|
||||
}
|
||||
|
||||
async function setRuntimeEmailState(email) {
|
||||
const normalizedEmail = String(email || '').trim() || null;
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'SET_EMAIL_STATE',
|
||||
source: 'sidepanel',
|
||||
payload: { email: normalizedEmail },
|
||||
});
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
return normalizedEmail;
|
||||
}
|
||||
|
||||
async function clearRegistrationEmail(options = {}) {
|
||||
const { silent = false } = options;
|
||||
if (!inputEmail.value.trim() && !latestState?.email) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputEmail.value = '';
|
||||
syncLatestState({ email: null });
|
||||
|
||||
try {
|
||||
await setRuntimeEmailState(null);
|
||||
} catch (err) {
|
||||
if (!silent) {
|
||||
showToast(`清空邮箱失败:${err.message}`, 'error');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function markSettingsDirty(isDirty = true) {
|
||||
settingsDirty = isDirty;
|
||||
updateSaveButtonState();
|
||||
@@ -516,6 +673,7 @@ function markSettingsDirty(isDirty = true) {
|
||||
|
||||
function updateSaveButtonState() {
|
||||
btnSaveSettings.disabled = settingsSaveInFlight || !settingsDirty;
|
||||
updateConfigMenuControls();
|
||||
btnSaveSettings.textContent = settingsSaveInFlight ? '保存中' : '保存';
|
||||
}
|
||||
|
||||
@@ -575,6 +733,9 @@ function applyAutoRunStatus(payload = currentAutoRun) {
|
||||
const locked = isAutoRunLockedPhase();
|
||||
const paused = isAutoRunPausedPhase();
|
||||
const scheduled = isAutoRunScheduledPhase();
|
||||
const settingsCardLocked = scheduled || locked;
|
||||
|
||||
setSettingsCardLocked(settingsCardLocked);
|
||||
|
||||
inputRunCount.disabled = currentAutoRun.autoRunning;
|
||||
btnAutoRun.disabled = currentAutoRun.autoRunning;
|
||||
@@ -591,6 +752,10 @@ function applyAutoRunStatus(payload = currentAutoRun) {
|
||||
autoContinueBar.style.display = 'none';
|
||||
btnAutoRun.innerHTML = `已计划${runLabel}`;
|
||||
break;
|
||||
case 'waiting_step':
|
||||
autoContinueBar.style.display = 'none';
|
||||
btnAutoRun.innerHTML = `等待中${runLabel}`;
|
||||
break;
|
||||
case 'waiting_email':
|
||||
autoContinueBar.style.display = 'flex';
|
||||
btnAutoRun.innerHTML = `已暂停${runLabel}`;
|
||||
@@ -616,6 +781,7 @@ function applyAutoRunStatus(payload = currentAutoRun) {
|
||||
updateAutoDelayInputState();
|
||||
syncScheduledCountdownTicker();
|
||||
updateStopButtonState(scheduled || paused || locked || Object.values(getStepStatuses()).some(status => status === 'running'));
|
||||
updateConfigMenuControls();
|
||||
}
|
||||
|
||||
function initializeManualStepActions() {
|
||||
@@ -653,11 +819,45 @@ function initializeManualStepActions() {
|
||||
// State Restore on load
|
||||
// ============================================================
|
||||
|
||||
function applySettingsState(state) {
|
||||
syncLatestState(state);
|
||||
syncAutoRunState(state);
|
||||
|
||||
inputEmail.value = state?.email || '';
|
||||
syncPasswordField(state || {});
|
||||
inputVpsUrl.value = state?.vpsUrl || '';
|
||||
inputVpsPassword.value = state?.vpsPassword || '';
|
||||
setLocalCpaStep9Mode(state?.localCpaStep9Mode);
|
||||
selectPanelMode.value = state?.panelMode || 'cpa';
|
||||
inputSub2ApiUrl.value = state?.sub2apiUrl || '';
|
||||
inputSub2ApiEmail.value = state?.sub2apiEmail || '';
|
||||
inputSub2ApiPassword.value = state?.sub2apiPassword || '';
|
||||
inputSub2ApiGroup.value = state?.sub2apiGroupName || '';
|
||||
selectMailProvider.value = state?.mailProvider || '163';
|
||||
selectEmailGenerator.value = state?.emailGenerator || 'duck';
|
||||
inputInbucketHost.value = state?.inbucketHost || '';
|
||||
inputInbucketMailbox.value = state?.inbucketMailbox || '';
|
||||
renderCloudflareDomainOptions(state?.cloudflareDomain || '');
|
||||
setCloudflareDomainEditMode(false, { clearInput: true });
|
||||
inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
|
||||
inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
|
||||
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes));
|
||||
if (state?.autoRunTotalRuns) {
|
||||
inputRunCount.value = String(state.autoRunTotalRuns);
|
||||
}
|
||||
|
||||
applyAutoRunStatus(state);
|
||||
markSettingsDirty(false);
|
||||
updateAutoDelayInputState();
|
||||
updatePanelModeUI();
|
||||
updateMailProviderUI();
|
||||
updateButtonStates();
|
||||
}
|
||||
|
||||
async function restoreState() {
|
||||
try {
|
||||
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' });
|
||||
syncLatestState(state);
|
||||
syncAutoRunState(state);
|
||||
applySettingsState(state);
|
||||
|
||||
if (state.oauthUrl) {
|
||||
displayOauthUrl.textContent = state.oauthUrl;
|
||||
@@ -667,52 +867,6 @@ async function restoreState() {
|
||||
displayLocalhostUrl.textContent = state.localhostUrl;
|
||||
displayLocalhostUrl.classList.add('has-value');
|
||||
}
|
||||
if (state.email) {
|
||||
inputEmail.value = state.email;
|
||||
}
|
||||
syncPasswordField(state);
|
||||
if (state.vpsUrl) {
|
||||
inputVpsUrl.value = state.vpsUrl;
|
||||
}
|
||||
if (state.vpsPassword) {
|
||||
inputVpsPassword.value = state.vpsPassword;
|
||||
}
|
||||
if (state.panelMode) {
|
||||
selectPanelMode.value = state.panelMode;
|
||||
}
|
||||
if (state.sub2apiUrl) {
|
||||
inputSub2ApiUrl.value = state.sub2apiUrl;
|
||||
}
|
||||
if (state.sub2apiEmail) {
|
||||
inputSub2ApiEmail.value = state.sub2apiEmail;
|
||||
}
|
||||
if (state.sub2apiPassword) {
|
||||
inputSub2ApiPassword.value = state.sub2apiPassword;
|
||||
}
|
||||
if (state.sub2apiGroupName) {
|
||||
inputSub2ApiGroup.value = state.sub2apiGroupName;
|
||||
}
|
||||
if (state.mailProvider) {
|
||||
selectMailProvider.value = state.mailProvider;
|
||||
}
|
||||
if (state.emailGenerator) {
|
||||
selectEmailGenerator.value = state.emailGenerator;
|
||||
}
|
||||
if (state.inbucketHost) {
|
||||
inputInbucketHost.value = state.inbucketHost;
|
||||
}
|
||||
if (state.inbucketMailbox) {
|
||||
inputInbucketMailbox.value = state.inbucketMailbox;
|
||||
}
|
||||
renderCloudflareDomainOptions(state.cloudflareDomain || '');
|
||||
setCloudflareDomainEditMode(false, { clearInput: true });
|
||||
inputAutoSkipFailures.checked = Boolean(state.autoRunSkipFailures);
|
||||
inputAutoDelayEnabled.checked = Boolean(state.autoRunDelayEnabled);
|
||||
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state.autoRunDelayMinutes));
|
||||
if (state.autoRunTotalRuns) {
|
||||
inputRunCount.value = String(state.autoRunTotalRuns);
|
||||
}
|
||||
|
||||
if (state.stepStatuses) {
|
||||
for (const [step, status] of Object.entries(state.stepStatuses)) {
|
||||
updateStepUI(Number(step), status);
|
||||
@@ -725,14 +879,8 @@ async function restoreState() {
|
||||
}
|
||||
}
|
||||
|
||||
applyAutoRunStatus(state);
|
||||
markSettingsDirty(false);
|
||||
updateAutoDelayInputState();
|
||||
updateStatusDisplay(latestState);
|
||||
updateProgressCounter();
|
||||
updatePanelModeUI();
|
||||
updateMailProviderUI();
|
||||
updateButtonStates();
|
||||
} catch (err) {
|
||||
console.error('Failed to restore state:', err);
|
||||
}
|
||||
@@ -1051,6 +1199,7 @@ function updatePanelModeUI() {
|
||||
const useSub2Api = selectPanelMode.value === 'sub2api';
|
||||
rowVpsUrl.style.display = useSub2Api ? 'none' : '';
|
||||
rowVpsPassword.style.display = useSub2Api ? 'none' : '';
|
||||
rowLocalCpaStep9Mode.style.display = useSub2Api ? 'none' : '';
|
||||
rowSub2ApiUrl.style.display = useSub2Api ? '' : 'none';
|
||||
rowSub2ApiEmail.style.display = useSub2Api ? '' : 'none';
|
||||
rowSub2ApiPassword.style.display = useSub2Api ? '' : 'none';
|
||||
@@ -1084,6 +1233,7 @@ function updateStepUI(step, status) {
|
||||
|
||||
updateButtonStates();
|
||||
updateProgressCounter();
|
||||
updateConfigMenuControls();
|
||||
}
|
||||
|
||||
function updateProgressCounter() {
|
||||
@@ -1166,6 +1316,15 @@ function updateStatusDisplay(state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAutoRunWaitingStepPhase()) {
|
||||
const runningSteps = getRunningSteps(state);
|
||||
displayStatus.textContent = runningSteps.length
|
||||
? `自动等待步骤 ${runningSteps.join(', ')} 完成后继续${getAutoRunLabel()}`
|
||||
: `自动正在按最新进度准备继续${getAutoRunLabel()}`;
|
||||
statusBar.classList.add('running');
|
||||
return;
|
||||
}
|
||||
|
||||
const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
|
||||
if (running) {
|
||||
displayStatus.textContent = `步骤 ${running[0]} 运行中...`;
|
||||
@@ -1295,6 +1454,93 @@ async function copyTextToClipboard(text) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
}
|
||||
|
||||
async function exportSettingsFile() {
|
||||
closeConfigMenu();
|
||||
configActionInFlight = true;
|
||||
updateConfigMenuControls();
|
||||
|
||||
try {
|
||||
await flushPendingSettingsBeforeExport();
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'EXPORT_SETTINGS',
|
||||
source: 'sidepanel',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (!response?.fileContent || !response?.fileName) {
|
||||
throw new Error('\u672a\u751f\u6210\u53ef\u4e0b\u8f7d\u7684\u914d\u7f6e\u6587\u4ef6\u3002');
|
||||
}
|
||||
|
||||
downloadTextFile(response.fileContent, response.fileName);
|
||||
showToast('\u914d\u7f6e\u5df2\u5bfc\u51fa\uff1a' + response.fileName, 'success', 2200);
|
||||
} catch (err) {
|
||||
showToast('\u5bfc\u51fa\u914d\u7f6e\u5931\u8d25\uff1a' + err.message, 'error');
|
||||
} finally {
|
||||
configActionInFlight = false;
|
||||
updateConfigMenuControls();
|
||||
}
|
||||
}
|
||||
|
||||
async function importSettingsFromFile(file) {
|
||||
if (!file) return;
|
||||
|
||||
configActionInFlight = true;
|
||||
closeConfigMenu();
|
||||
updateConfigMenuControls();
|
||||
|
||||
try {
|
||||
await settlePendingSettingsBeforeImport();
|
||||
const rawText = await file.text();
|
||||
|
||||
let parsedConfig = null;
|
||||
try {
|
||||
parsedConfig = JSON.parse(rawText);
|
||||
} catch {
|
||||
throw new Error('\u914d\u7f6e\u6587\u4ef6\u4e0d\u662f\u6709\u6548\u7684 JSON\u3002');
|
||||
}
|
||||
|
||||
const confirmed = await openConfirmModal({
|
||||
title: '\u5bfc\u5165\u914d\u7f6e',
|
||||
message: '\u786e\u8ba4\u5bfc\u5165\u914d\u7f6e\u6587\u4ef6 "' + file.name + '" \u5417\uff1f\u5bfc\u5165\u540e\u4f1a\u8986\u76d6\u5f53\u524d\u914d\u7f6e\u3002',
|
||||
confirmLabel: '\u786e\u8ba4\u8986\u76d6\u5bfc\u5165',
|
||||
confirmVariant: 'btn-danger',
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'IMPORT_SETTINGS',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
config: parsedConfig,
|
||||
},
|
||||
});
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (!response?.state) {
|
||||
throw new Error('\u5bfc\u5165\u540e\u672a\u8fd4\u56de\u6700\u65b0\u914d\u7f6e\u72b6\u6001\u3002');
|
||||
}
|
||||
|
||||
applySettingsState(response.state);
|
||||
updateStatusDisplay(latestState);
|
||||
showToast('\u914d\u7f6e\u5df2\u5bfc\u5165\uff0c\u5f53\u524d\u914d\u7f6e\u5df2\u8986\u76d6\u3002', 'success', 2200);
|
||||
} catch (err) {
|
||||
showToast('\u5bfc\u5165\u914d\u7f6e\u5931\u8d25\uff1a' + err.message, 'error');
|
||||
} finally {
|
||||
configActionInFlight = false;
|
||||
updateConfigMenuControls();
|
||||
if (inputImportSettingsFile) {
|
||||
inputImportSettingsFile.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHotmailAccountsByMode(mode) {
|
||||
const isUsedMode = mode === 'used';
|
||||
const targetAccounts = getHotmailAccountsByUsage(isUsedMode ? 'used' : 'all');
|
||||
@@ -1362,6 +1608,13 @@ function syncVpsUrlToggleLabel() {
|
||||
});
|
||||
}
|
||||
|
||||
function syncVpsPasswordToggleLabel() {
|
||||
syncToggleButtonLabel(btnToggleVpsPassword, inputVpsPassword, {
|
||||
show: '显示管理密钥',
|
||||
hide: '隐藏管理密钥',
|
||||
});
|
||||
}
|
||||
|
||||
async function maybeTakeoverAutoRun(actionLabel) {
|
||||
if (!isAutoRunPausedPhase()) {
|
||||
return true;
|
||||
@@ -1698,6 +1951,23 @@ btnToggleVpsUrl.addEventListener('click', () => {
|
||||
syncVpsUrlToggleLabel();
|
||||
});
|
||||
|
||||
btnToggleVpsPassword.addEventListener('click', () => {
|
||||
inputVpsPassword.type = inputVpsPassword.type === 'password' ? 'text' : 'password';
|
||||
syncVpsPasswordToggleLabel();
|
||||
});
|
||||
|
||||
localCpaStep9ModeButtons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const nextMode = button.dataset.localCpaStep9Mode;
|
||||
if (getSelectedLocalCpaStep9Mode() === normalizeLocalCpaStep9Mode(nextMode)) {
|
||||
return;
|
||||
}
|
||||
setLocalCpaStep9Mode(nextMode);
|
||||
markSettingsDirty(true);
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
});
|
||||
|
||||
btnSaveSettings.addEventListener('click', async () => {
|
||||
if (!settingsDirty) {
|
||||
showToast('配置已是最新', 'info', 1400);
|
||||
@@ -1712,6 +1982,38 @@ btnStop.addEventListener('click', async () => {
|
||||
showToast(isAutoRunScheduledPhase() ? '正在取消倒计时计划...' : '正在停止当前流程...', 'warn', 2000);
|
||||
});
|
||||
|
||||
btnConfigMenu?.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
toggleConfigMenu();
|
||||
});
|
||||
|
||||
configMenu?.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
btnExportSettings?.addEventListener('click', async () => {
|
||||
if (configActionInFlight || settingsSaveInFlight) {
|
||||
return;
|
||||
}
|
||||
await exportSettingsFile();
|
||||
});
|
||||
|
||||
btnImportSettings?.addEventListener('click', async () => {
|
||||
if (configActionInFlight || settingsSaveInFlight) {
|
||||
return;
|
||||
}
|
||||
closeConfigMenu();
|
||||
if (inputImportSettingsFile) {
|
||||
inputImportSettingsFile.value = '';
|
||||
inputImportSettingsFile.click();
|
||||
}
|
||||
});
|
||||
|
||||
inputImportSettingsFile?.addEventListener('change', async () => {
|
||||
const file = inputImportSettingsFile.files?.[0] || null;
|
||||
await importSettingsFromFile(file);
|
||||
});
|
||||
|
||||
autoStartModal?.addEventListener('click', (event) => {
|
||||
if (event.target === autoStartModal) {
|
||||
resolveModalChoice(null);
|
||||
@@ -1727,7 +2029,8 @@ btnAutoRun.addEventListener('click', async () => {
|
||||
|
||||
if (shouldOfferAutoModeChoice()) {
|
||||
const startStep = getFirstUnfinishedStep();
|
||||
const choice = await openAutoStartChoiceDialog(startStep);
|
||||
const runningStep = getRunningSteps()[0] ?? null;
|
||||
const choice = await openAutoStartChoiceDialog(startStep, { runningStep });
|
||||
if (!choice) {
|
||||
return;
|
||||
}
|
||||
@@ -1847,8 +2150,18 @@ inputEmail.addEventListener('change', async () => {
|
||||
return;
|
||||
}
|
||||
const email = inputEmail.value.trim();
|
||||
if (email) {
|
||||
await chrome.runtime.sendMessage({ type: 'SAVE_EMAIL', source: 'sidepanel', payload: { email } });
|
||||
inputEmail.value = email;
|
||||
try {
|
||||
if (email) {
|
||||
const response = await chrome.runtime.sendMessage({ type: 'SAVE_EMAIL', source: 'sidepanel', payload: { email } });
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
} else {
|
||||
await setRuntimeEmailState(null);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
});
|
||||
inputEmail.addEventListener('input', updateButtonStates);
|
||||
@@ -1885,6 +2198,7 @@ selectMailProvider.addEventListener('change', () => {
|
||||
|
||||
selectEmailGenerator.addEventListener('change', () => {
|
||||
updateMailProviderUI();
|
||||
clearRegistrationEmail({ silent: true }).catch(() => { });
|
||||
markSettingsDirty(true);
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
@@ -2072,6 +2386,9 @@ chrome.runtime.onMessage.addListener((message) => {
|
||||
if (message.payload.password !== undefined) {
|
||||
inputPassword.value = message.payload.password || '';
|
||||
}
|
||||
if (message.payload.localCpaStep9Mode !== undefined) {
|
||||
setLocalCpaStep9Mode(message.payload.localCpaStep9Mode);
|
||||
}
|
||||
if (message.payload.oauthUrl !== undefined) {
|
||||
displayOauthUrl.textContent = message.payload.oauthUrl || '等待中...';
|
||||
displayOauthUrl.classList.toggle('has-value', Boolean(message.payload.oauthUrl));
|
||||
@@ -2099,7 +2416,7 @@ chrome.runtime.onMessage.addListener((message) => {
|
||||
|
||||
case 'AUTO_RUN_STATUS': {
|
||||
syncLatestState({
|
||||
autoRunning: ['scheduled', 'running', 'waiting_email', 'retrying'].includes(message.payload.phase),
|
||||
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying'].includes(message.payload.phase),
|
||||
autoRunPhase: message.payload.phase,
|
||||
autoRunCurrentRun: message.payload.currentRun,
|
||||
autoRunTotalRuns: message.payload.totalRuns,
|
||||
@@ -2139,6 +2456,22 @@ btnTheme.addEventListener('click', () => {
|
||||
setTheme(current === 'dark' ? 'light' : 'dark');
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!configMenuOpen) {
|
||||
return;
|
||||
}
|
||||
if (configMenuShell?.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
closeConfigMenu();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && configMenuOpen) {
|
||||
closeConfigMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Init
|
||||
// ============================================================
|
||||
@@ -2147,9 +2480,12 @@ initializeManualStepActions();
|
||||
initTheme();
|
||||
initHotmailListExpandedState();
|
||||
updateSaveButtonState();
|
||||
updateConfigMenuControls();
|
||||
setLocalCpaStep9Mode(DEFAULT_LOCAL_CPA_STEP9_MODE);
|
||||
restoreState().then(() => {
|
||||
syncPasswordToggleLabel();
|
||||
syncVpsUrlToggleLabel();
|
||||
syncVpsPasswordToggleLabel();
|
||||
updatePanelModeUI();
|
||||
updateButtonStates();
|
||||
updateStatusDisplay(latestState);
|
||||
|
||||
@@ -28,29 +28,42 @@ function extractFunction(name) {
|
||||
}
|
||||
|
||||
const bundle = [
|
||||
'const DEFAULT_LOCAL_CPA_STEP9_MODE = "submit";',
|
||||
extractFunction('parseUrlSafely'),
|
||||
extractFunction('isLocalCpaUrl'),
|
||||
extractFunction('normalizeLocalCpaStep9Mode'),
|
||||
extractFunction('shouldBypassStep9ForLocalCpa'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`${bundle}; return { isLocalCpaUrl, shouldBypassStep9ForLocalCpa };`)();
|
||||
const api = new Function(`${bundle}; return { isLocalCpaUrl, normalizeLocalCpaStep9Mode, shouldBypassStep9ForLocalCpa };`)();
|
||||
|
||||
assert.strictEqual(api.isLocalCpaUrl('http://127.0.0.1:8317/management.html#/oauth'), true, '127.0.0.1 应视为本地 CPA');
|
||||
assert.strictEqual(api.isLocalCpaUrl('http://localhost:1455/management.html#/oauth'), true, 'localhost 应视为本地 CPA');
|
||||
assert.strictEqual(api.isLocalCpaUrl('https://example.com/management.html#/oauth'), false, '远程域名不应视为本地 CPA');
|
||||
assert.strictEqual(api.isLocalCpaUrl('notaurl'), false, '非法 URL 不应视为本地 CPA');
|
||||
assert.strictEqual(api.normalizeLocalCpaStep9Mode('submit'), 'submit', 'submit 应保持为 submit');
|
||||
assert.strictEqual(api.normalizeLocalCpaStep9Mode('bypass'), 'bypass', 'bypass 应保持为 bypass');
|
||||
assert.strictEqual(api.normalizeLocalCpaStep9Mode('other'), 'submit', '未知模式应回退为 submit');
|
||||
|
||||
assert.strictEqual(api.shouldBypassStep9ForLocalCpa({
|
||||
vpsUrl: 'http://127.0.0.1:8317/management.html#/oauth',
|
||||
localhostUrl: 'http://127.0.0.1:8317/codex/callback?code=abc&state=xyz',
|
||||
}), true, '本地 CPA 且已有 callback 时应跳过远程提交流程');
|
||||
}), false, '默认模式下,本地 CPA 也应执行步骤 9');
|
||||
|
||||
assert.strictEqual(api.shouldBypassStep9ForLocalCpa({
|
||||
localCpaStep9Mode: 'bypass',
|
||||
vpsUrl: 'http://127.0.0.1:8317/management.html#/oauth',
|
||||
localhostUrl: 'http://127.0.0.1:8317/codex/callback?code=abc&state=xyz',
|
||||
}), true, '切换为 bypass 后,本地 CPA 且已有 callback 时应跳过步骤 9');
|
||||
|
||||
assert.strictEqual(api.shouldBypassStep9ForLocalCpa({
|
||||
localCpaStep9Mode: 'bypass',
|
||||
vpsUrl: 'https://example.com/management.html#/oauth',
|
||||
localhostUrl: 'http://127.0.0.1:8317/codex/callback?code=abc&state=xyz',
|
||||
}), false, '远程 CPA 不应跳过步骤 9');
|
||||
|
||||
assert.strictEqual(api.shouldBypassStep9ForLocalCpa({
|
||||
localCpaStep9Mode: 'bypass',
|
||||
vpsUrl: 'http://127.0.0.1:8317/management.html#/oauth',
|
||||
localhostUrl: '',
|
||||
}), false, '没有 callback 时不应跳过步骤 9');
|
||||
|
||||
Reference in New Issue
Block a user