Merge branch 'dev'

This commit is contained in:
QLHazyCoder
2026-04-13 01:24:40 +08:00
6 changed files with 822 additions and 125 deletions
+4
View File
@@ -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
View File
@@ -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',
+118
View File
@@ -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;
+28 -8
View File
@@ -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
View File
@@ -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);
+15 -2
View File
@@ -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');