merge: 合并 dev 并解决 PR 冲突

This commit is contained in:
Ryan Liu
2026-04-23 10:05:23 +08:00
98 changed files with 17803 additions and 912 deletions
+58
View File
@@ -0,0 +1,58 @@
(function attachSidepanelAccountPoolUi(globalScope) {
function createAccountPoolFormController(options = {}) {
const {
formShell = null,
toggleButton = null,
hiddenLabel = '添加账号',
visibleLabel = '取消添加',
onClear = null,
onFocus = null,
} = options;
let visible = false;
function sync() {
if (formShell) {
formShell.hidden = !visible;
}
if (toggleButton) {
toggleButton.textContent = visible ? visibleLabel : hiddenLabel;
toggleButton.setAttribute('aria-expanded', String(visible));
}
}
function setVisible(nextVisible, controlOptions = {}) {
const {
clearForm = false,
focusField = false,
} = controlOptions;
visible = Boolean(nextVisible);
if (clearForm && typeof onClear === 'function') {
onClear();
}
sync();
if (visible && focusField && typeof onFocus === 'function') {
onFocus();
}
}
function isVisible() {
return visible;
}
sync();
return {
isVisible,
setVisible,
sync,
};
}
globalScope.SidepanelAccountPoolUi = {
createAccountPoolFormController,
};
})(window);
@@ -0,0 +1,147 @@
(() => {
const PORTAL_BASE_URL = 'https://apikey.qzz.io';
const CONTENT_SUMMARY_API_URL = `${PORTAL_BASE_URL}/api/content-summary`;
const CACHE_KEY = 'multipage-contribution-content-summary-v1';
const FETCH_TIMEOUT_MS = 6000;
function sanitizeItem(item = {}) {
return {
slug: String(item?.slug || '').trim(),
title: String(item?.title || '').trim(),
isEnabled: Boolean(item?.is_enabled),
hasContent: Boolean(item?.has_content),
isVisible: Boolean(item?.is_visible),
updatedAt: String(item?.updated_at || '').trim(),
updatedAtDisplay: String(item?.updated_at_display || '').trim(),
};
}
function buildSnapshot(payload = {}) {
const items = Array.isArray(payload?.items)
? payload.items.map(sanitizeItem).filter((item) => item.slug)
: [];
const promptVersion = String(payload?.prompt_version || '').trim();
const latestUpdatedAt = String(payload?.latest_updated_at || '').trim();
const latestUpdatedAtDisplay = String(payload?.latest_updated_at_display || '').trim();
const hasVisibleUpdates = Boolean(payload?.has_visible_updates) && Boolean(promptVersion);
return {
status: hasVisibleUpdates ? 'update-available' : 'idle',
promptVersion,
hasVisibleUpdates,
latestUpdatedAt,
latestUpdatedAtDisplay,
items,
portalUrl: PORTAL_BASE_URL,
apiUrl: CONTENT_SUMMARY_API_URL,
checkedAt: Date.now(),
};
}
function readCache() {
try {
const raw = localStorage.getItem(CACHE_KEY);
if (!raw) {
return null;
}
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') {
return null;
}
const snapshot = buildSnapshot({
items: parsed.items,
prompt_version: parsed.promptVersion,
has_visible_updates: parsed.hasVisibleUpdates,
latest_updated_at: parsed.latestUpdatedAt,
latest_updated_at_display: parsed.latestUpdatedAtDisplay,
});
if (!Number.isFinite(parsed.checkedAt)) {
return snapshot;
}
snapshot.checkedAt = parsed.checkedAt;
return snapshot;
} catch (error) {
return null;
}
}
function writeCache(snapshot) {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify(snapshot));
} catch (error) {
// Ignore cache write failures.
}
}
async function fetchContentSummary() {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const response = await fetch(CONTENT_SUMMARY_API_URL, {
method: 'GET',
headers: {
Accept: 'application/json',
},
cache: 'no-store',
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`内容摘要请求失败:${response.status}`);
}
const payload = await response.json();
if (!payload || payload.ok !== true) {
throw new Error('内容摘要返回格式异常');
}
const snapshot = buildSnapshot(payload);
writeCache(snapshot);
return snapshot;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error('内容摘要请求超时');
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
async function getContentUpdateSnapshot() {
try {
return await fetchContentSummary();
} catch (error) {
const cached = readCache();
if (cached) {
return {
...cached,
fromCache: true,
errorMessage: error?.message || '内容摘要获取失败',
};
}
return {
status: 'error',
promptVersion: '',
hasVisibleUpdates: false,
latestUpdatedAt: '',
latestUpdatedAtDisplay: '',
items: [],
portalUrl: PORTAL_BASE_URL,
apiUrl: CONTENT_SUMMARY_API_URL,
checkedAt: Date.now(),
errorMessage: error?.message || '内容摘要获取失败',
};
}
}
window.SidepanelContributionContentService = {
getContentUpdateSnapshot,
portalUrl: PORTAL_BASE_URL,
apiUrl: CONTENT_SUMMARY_API_URL,
};
})();
+471
View File
@@ -0,0 +1,471 @@
(function attachSidepanelContributionMode(globalScope) {
const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']);
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'manual_review_required', 'expired', 'error']);
const DEFAULT_COPY = '当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,并继续等待 CPA 最终确认。';
function createContributionModeManager(context = {}) {
const {
state,
dom,
helpers,
runtime,
constants = {},
} = context;
const contributionUploadUrl = constants.contributionUploadUrl || 'https://apikey.qzz.io';
const pollIntervalMs = Math.max(1500, Math.floor(Number(constants.pollIntervalMs) || 2500));
const hiddenRows = [
dom.rowVpsUrl,
dom.rowVpsPassword,
dom.rowLocalCpaStep9Mode,
dom.rowSub2ApiUrl,
dom.rowSub2ApiEmail,
dom.rowSub2ApiPassword,
dom.rowSub2ApiGroup,
dom.rowSub2ApiDefaultProxy,
dom.rowCodex2ApiUrl,
dom.rowCodex2ApiAdminKey,
dom.rowCustomPassword,
dom.rowAccountRunHistoryHelperBaseUrl,
].filter(Boolean);
let actionInFlight = false;
let pollInFlight = false;
let pollTimer = null;
function getLatestState() {
return state.getLatestState?.() || {};
}
function normalizeString(value = '') {
return String(value || '').trim();
}
function normalizeStatus(value = '') {
const normalized = normalizeString(value).toLowerCase();
if (ACTIVE_STATUSES.has(normalized) || FINAL_STATUSES.has(normalized)) {
return normalized;
}
return '';
}
function normalizeCallbackStatus(value = '') {
const normalized = normalizeString(value).toLowerCase();
switch (normalized) {
case 'waiting':
case 'captured':
case 'submitting':
case 'submitted':
case 'failed':
case 'idle':
return normalized;
default:
return '';
}
}
function isContributionModeEnabled(currentState = getLatestState()) {
return Boolean(currentState.contributionMode);
}
function hasActiveContributionSession(currentState = getLatestState()) {
const status = normalizeStatus(currentState.contributionStatus);
return Boolean(normalizeString(currentState.contributionSessionId) && status && !FINAL_STATUSES.has(status));
}
function isModeSwitchBlocked() {
return Boolean(helpers.isModeSwitchBlocked?.(getLatestState()));
}
function setContributionHidden(element, hidden) {
element?.classList.toggle('is-contribution-hidden', hidden);
}
function syncContributionRows(enabled) {
hiddenRows.forEach((row) => {
setContributionHidden(row, enabled);
});
}
function syncContributionButton(enabled, blocked) {
if (!dom.btnContributionMode) {
return;
}
dom.btnContributionMode.classList.toggle('is-active', enabled);
dom.btnContributionMode.setAttribute('aria-pressed', String(enabled));
dom.btnContributionMode.disabled = enabled || blocked;
dom.btnContributionMode.title = blocked
? '当前流程运行中,暂时不能切换贡献模式'
: (enabled ? '当前已在贡献模式' : '进入贡献模式');
}
function stopPolling() {
if (pollTimer) {
clearTimeout(pollTimer);
pollTimer = null;
}
}
function schedulePolling(delayMs = pollIntervalMs) {
stopPolling();
if (!isContributionModeEnabled() || !hasActiveContributionSession()) {
return;
}
pollTimer = setTimeout(() => {
pollOnce({ silentError: true }).catch(() => {});
}, delayMs);
}
function ensurePolling() {
if (!isContributionModeEnabled() || !hasActiveContributionSession()) {
stopPolling();
return;
}
if (!pollTimer && !pollInFlight) {
schedulePolling(1200);
}
}
function getOauthStatusText(currentState = getLatestState()) {
const status = normalizeStatus(currentState.contributionStatus);
const hasAuthUrl = Boolean(normalizeString(currentState.contributionAuthUrl));
if (!normalizeString(currentState.contributionSessionId) || !hasAuthUrl) {
return '未生成登录地址';
}
if (status === 'waiting') {
return '等待提交回调';
}
if (status === 'processing' || status === 'auto_approved' || status === 'auto_rejected' || status === 'manual_review_required') {
return status === 'processing' ? '已提交回调' : '授权已结束';
}
if (status === 'expired' || status === 'error') {
return '授权失败';
}
if (Number(currentState.contributionAuthOpenedAt) > 0) {
return '已打开授权页';
}
return '登录地址已生成';
}
function getCallbackStatusText(currentState = getLatestState()) {
const status = normalizeCallbackStatus(currentState.contributionCallbackStatus);
switch (status) {
case 'captured':
return '已捕获回调地址';
case 'submitting':
return '正在提交回调';
case 'submitted':
return '已提交回调';
case 'failed':
return '回调提交失败';
case 'waiting':
case 'idle':
default:
return normalizeString(currentState.contributionCallbackUrl)
? '已捕获回调地址'
: '等待回调';
}
}
function getSummaryText(currentState = getLatestState()) {
return normalizeString(currentState.contributionStatusMessage) || DEFAULT_COPY;
}
function getContributionUploadPageUrl() {
return normalizeString(contributionUploadUrl);
}
function openContributionUploadPage() {
const targetUrl = getContributionUploadPageUrl();
if (!targetUrl) {
return;
}
helpers.openExternalUrl?.(targetUrl);
}
async function syncContributionProfile(partial = {}) {
const payload = {
nickname: normalizeString(partial.nickname),
qq: normalizeString(partial.qq),
};
const response = await runtime.sendMessage({
type: 'SET_CONTRIBUTION_PROFILE',
source: 'sidepanel',
payload,
});
if (response?.error) {
throw new Error(response.error);
}
if (response?.state) {
helpers.applySettingsState?.(response.state);
}
}
async function requestContributionMode(enabled) {
const response = await runtime.sendMessage({
type: 'SET_CONTRIBUTION_MODE',
source: 'sidepanel',
payload: { enabled: Boolean(enabled) },
});
if (response?.error) {
throw new Error(response.error);
}
if (!response?.state) {
throw new Error('贡献模式切换后未返回最新状态。');
}
helpers.applySettingsState?.(response.state);
helpers.updateStatusDisplay?.(response.state);
render();
}
async function pollOnce(options = {}) {
if (pollInFlight || !isContributionModeEnabled() || !hasActiveContributionSession()) {
if (!hasActiveContributionSession()) {
stopPolling();
}
return;
}
pollInFlight = true;
try {
const response = await runtime.sendMessage({
type: 'POLL_CONTRIBUTION_STATUS',
source: 'sidepanel',
payload: {
reason: options.reason || 'sidepanel_poll',
},
});
if (response?.error) {
throw new Error(response.error);
}
if (response?.state) {
helpers.applySettingsState?.(response.state);
helpers.updateStatusDisplay?.(response.state);
}
} finally {
pollInFlight = false;
render();
if (hasActiveContributionSession()) {
schedulePolling();
} else {
stopPolling();
}
}
}
async function startContributionFlow() {
if (typeof helpers.startContributionAutoRun !== 'function') {
throw new Error('贡献模式尚未接入主自动流程启动能力。');
}
const profile = helpers.getContributionProfile?.() || {};
const qq = normalizeString(profile.qq);
if (qq && !/^\d{1,20}$/.test(qq)) {
throw new Error('QQ 只能填写数字,且长度不能超过 20 位。');
}
await syncContributionProfile(profile);
const started = await helpers.startContributionAutoRun();
if (!started) {
return;
}
helpers.showToast?.('贡献自动流程已启动。', 'info', 1800);
render();
}
async function enterContributionMode() {
await requestContributionMode(true);
helpers.showToast?.('已进入贡献模式。', 'success', 1800);
}
async function exitContributionMode() {
stopPolling();
await requestContributionMode(false);
helpers.showToast?.('已退出贡献模式。', 'info', 1800);
}
function render() {
const currentState = getLatestState();
const enabled = isContributionModeEnabled(currentState);
const blocked = isModeSwitchBlocked();
const activeElement = typeof document !== 'undefined' ? document.activeElement : null;
if (enabled && dom.selectPanelMode) {
dom.selectPanelMode.value = 'cpa';
}
helpers.updatePanelModeUI?.();
helpers.updateAccountRunHistorySettingsUI?.();
if (dom.contributionModePanel) {
dom.contributionModePanel.hidden = !enabled;
}
if (dom.contributionModeText) {
dom.contributionModeText.textContent = DEFAULT_COPY;
}
if (dom.inputContributionNickname && activeElement !== dom.inputContributionNickname) {
const nextNickname = normalizeString(currentState.contributionNickname);
if (nextNickname || !normalizeString(dom.inputContributionNickname.value)) {
dom.inputContributionNickname.value = nextNickname;
}
}
if (dom.inputContributionQq && activeElement !== dom.inputContributionQq) {
const nextQq = normalizeString(currentState.contributionQq);
if (nextQq || !normalizeString(dom.inputContributionQq.value)) {
dom.inputContributionQq.value = nextQq;
}
}
if (dom.contributionOauthStatus) {
dom.contributionOauthStatus.textContent = getOauthStatusText(currentState);
}
if (dom.contributionCallbackStatus) {
dom.contributionCallbackStatus.textContent = getCallbackStatusText(currentState);
}
if (dom.contributionModeSummary) {
dom.contributionModeSummary.textContent = getSummaryText(currentState);
}
syncContributionRows(enabled);
syncContributionButton(enabled, blocked);
if (dom.selectPanelMode) {
dom.selectPanelMode.disabled = enabled;
}
if (dom.btnStartContribution) {
dom.btnStartContribution.disabled = actionInFlight || blocked;
}
if (dom.btnOpenContributionUpload) {
dom.btnOpenContributionUpload.disabled = false;
}
if (dom.btnExitContributionMode) {
dom.btnExitContributionMode.disabled = actionInFlight || blocked;
dom.btnExitContributionMode.title = blocked ? '当前流程运行中,暂时不能退出贡献模式' : '退出贡献模式';
}
if (dom.btnOpenAccountRecords) {
dom.btnOpenAccountRecords.disabled = enabled;
}
if (enabled) {
helpers.closeConfigMenu?.();
helpers.closeAccountRecordsPanel?.();
ensurePolling();
} else {
stopPolling();
}
helpers.updateConfigMenuControls?.();
}
function bindEvents() {
dom.btnContributionMode?.addEventListener('click', async () => {
if (actionInFlight) {
return;
}
actionInFlight = true;
try {
openContributionUploadPage();
} catch (error) {
helpers.showToast?.(`打开上传页面失败:${error.message}`, 'error');
}
render();
try {
await enterContributionMode();
} catch (error) {
helpers.showToast?.(error.message, 'error');
} finally {
actionInFlight = false;
render();
}
});
dom.btnStartContribution?.addEventListener('click', async () => {
if (actionInFlight) {
return;
}
actionInFlight = true;
render();
try {
await startContributionFlow();
} catch (error) {
helpers.showToast?.(error.message, 'error');
} finally {
actionInFlight = false;
render();
}
});
dom.inputContributionNickname?.addEventListener('change', async () => {
try {
await syncContributionProfile({
nickname: dom.inputContributionNickname?.value,
qq: dom.inputContributionQq?.value,
});
} catch (error) {
helpers.showToast?.(error.message, 'error');
} finally {
render();
}
});
dom.inputContributionQq?.addEventListener('change', async () => {
try {
await syncContributionProfile({
nickname: dom.inputContributionNickname?.value,
qq: dom.inputContributionQq?.value,
});
} catch (error) {
helpers.showToast?.(error.message, 'error');
} finally {
render();
}
});
dom.btnOpenContributionUpload?.addEventListener('click', () => {
try {
openContributionUploadPage();
} catch (error) {
helpers.showToast?.(`打开上传页面失败:${error.message}`, 'error');
}
});
dom.btnExitContributionMode?.addEventListener('click', async () => {
if (actionInFlight) {
return;
}
actionInFlight = true;
render();
try {
await exitContributionMode();
} catch (error) {
helpers.showToast?.(error.message, 'error');
} finally {
actionInFlight = false;
render();
}
});
}
return {
bindEvents,
pollOnce,
render,
stopPolling,
};
}
globalScope.SidepanelContributionMode = {
createContributionModeManager,
};
})(typeof window !== 'undefined' ? window : globalThis);
+30 -1
View File
@@ -12,6 +12,7 @@
const expandedStorageKey = constants.expandedStorageKey || 'multipage-hotmail-list-expanded';
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
const copyIcon = constants.copyIcon || '';
const createAccountPoolFormController = globalScope.SidepanelAccountPoolUi?.createAccountPoolFormController;
let actionInFlight = false;
let listExpanded = false;
@@ -177,6 +178,25 @@
dom.inputHotmailRefreshToken.value = '';
}
const formController = typeof createAccountPoolFormController === 'function'
? createAccountPoolFormController({
formShell: dom.hotmailFormShell,
toggleButton: dom.btnToggleHotmailForm,
hiddenLabel: '添加账号',
visibleLabel: '取消添加',
onClear: () => {
clearHotmailForm();
},
onFocus: () => {
dom.inputHotmailEmail?.focus?.();
},
})
: {
isVisible: () => false,
setVisible() {},
sync() {},
};
function renderHotmailAccounts() {
if (!dom.hotmailAccountsList) return;
const latestState = state.getLatestState();
@@ -318,7 +338,7 @@
}
helpers.showToast(`已保存 Hotmail 账号 ${email}`, 'success', 1800);
clearHotmailForm();
formController.setVisible(false, { clearForm: true });
} catch (err) {
helpers.showToast(`保存 Hotmail 账号失败:${err.message}`, 'error');
} finally {
@@ -474,6 +494,14 @@
setHotmailListExpanded(!listExpanded);
});
dom.btnToggleHotmailForm?.addEventListener('click', () => {
if (formController.isVisible()) {
formController.setVisible(false, { clearForm: true });
return;
}
formController.setVisible(true, { focusField: true });
});
dom.btnHotmailUsageGuide?.addEventListener('click', async () => {
await helpers.openConfirmModal({
title: '使用教程',
@@ -514,6 +542,7 @@
dom.btnAddHotmailAccount?.addEventListener('click', handleAddHotmailAccount);
dom.btnImportHotmailAccounts?.addEventListener('click', handleImportHotmailAccounts);
dom.hotmailAccountsList?.addEventListener('click', handleAccountListClick);
formController.sync();
}
return {
+538
View File
@@ -0,0 +1,538 @@
(function attachSidepanelMail2925Manager(globalScope) {
function createMail2925Manager(context = {}) {
const {
state,
dom,
helpers,
runtime,
constants = {},
mail2925Utils = {},
} = context;
const expandedStorageKey = constants.expandedStorageKey || 'multipage-mail2925-list-expanded';
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
const copyIcon = constants.copyIcon || '';
const createAccountPoolFormController = globalScope.SidepanelAccountPoolUi?.createAccountPoolFormController;
let actionInFlight = false;
let listExpanded = false;
let editingAccountId = '';
function getMail2925Accounts(currentState = state.getLatestState()) {
return helpers.getMail2925Accounts(currentState);
}
function getCurrentMail2925AccountId(currentState = state.getLatestState()) {
return String(currentState?.currentMail2925AccountId || '');
}
function updateMail2925ListViewport() {
const count = getMail2925Accounts().length;
if (dom.btnDeleteAllMail2925Accounts) {
dom.btnDeleteAllMail2925Accounts.textContent = `全部删除${count > 0 ? `${count}` : ''}`;
dom.btnDeleteAllMail2925Accounts.disabled = count === 0;
}
if (dom.btnToggleMail2925List) {
const label = typeof mail2925Utils.getMail2925ListToggleLabel === 'function'
? mail2925Utils.getMail2925ListToggleLabel(listExpanded, count)
: `${listExpanded ? '收起列表' : '展开列表'}${count > 0 ? `${count}` : ''}`;
dom.btnToggleMail2925List.textContent = label;
dom.btnToggleMail2925List.setAttribute('aria-expanded', String(listExpanded));
dom.btnToggleMail2925List.disabled = count === 0;
}
if (dom.mail2925ListShell) {
dom.mail2925ListShell.classList.toggle('is-expanded', listExpanded);
dom.mail2925ListShell.classList.toggle('is-collapsed', !listExpanded);
}
}
function setMail2925ListExpanded(expanded, options = {}) {
const { persist = true } = options;
listExpanded = Boolean(expanded);
updateMail2925ListViewport();
if (persist) {
localStorage.setItem(expandedStorageKey, listExpanded ? '1' : '0');
}
}
function initMail2925ListExpandedState() {
const saved = localStorage.getItem(expandedStorageKey);
setMail2925ListExpanded(saved === '1', { persist: false });
}
function formatDateTime(timestamp) {
const value = Number(timestamp);
if (!Number.isFinite(value) || value <= 0) {
return '未记录';
}
return new Date(value).toLocaleString('zh-CN', {
hour12: false,
timeZone: displayTimeZone,
});
}
function getStatusSnapshot(account) {
const status = typeof mail2925Utils.getMail2925AccountStatus === 'function'
? mail2925Utils.getMail2925AccountStatus(account, Date.now())
: 'ready';
switch (status) {
case 'cooldown':
return { label: '冷却中', className: 'status-used' };
case 'disabled':
return { label: '已禁用', className: 'status-disabled' };
case 'error':
return { label: '异常', className: 'status-error' };
case 'pending':
return { label: '待完善', className: 'status-pending' };
default:
return { label: '可用', className: 'status-authorized' };
}
}
function refreshManagedAliasBaseEmail() {
if (typeof helpers.refreshManagedAliasBaseEmail === 'function') {
helpers.refreshManagedAliasBaseEmail();
}
}
function applyMail2925AccountMutation(account) {
if (!account?.id) return;
const latestState = state.getLatestState();
const currentId = getCurrentMail2925AccountId(latestState);
const nextAccounts = typeof mail2925Utils.upsertMail2925AccountInList === 'function'
? mail2925Utils.upsertMail2925AccountInList(getMail2925Accounts(latestState), account)
: getMail2925Accounts(latestState).map((item) => (item.id === account.id ? account : item));
const nextState = {
mail2925Accounts: nextAccounts,
};
if (currentId === account.id && account.enabled === false) {
nextState.currentMail2925AccountId = null;
}
state.syncLatestState(nextState);
refreshManagedAliasBaseEmail();
renderMail2925Accounts();
}
function clearMail2925Form() {
if (dom.inputMail2925Email) dom.inputMail2925Email.value = '';
if (dom.inputMail2925Password) dom.inputMail2925Password.value = '';
}
const formController = typeof createAccountPoolFormController === 'function'
? createAccountPoolFormController({
formShell: dom.mail2925FormShell,
toggleButton: dom.btnToggleMail2925Form,
hiddenLabel: '添加账号',
visibleLabel: '取消添加',
onClear: () => {
stopEditingAccount({ clearForm: true });
},
onFocus: () => {
dom.inputMail2925Email?.focus?.();
},
})
: {
isVisible: () => false,
setVisible() {},
sync() {},
};
function syncEditUi() {
if (dom.btnAddMail2925Account) {
dom.btnAddMail2925Account.textContent = editingAccountId ? '保存修改' : '添加账号';
}
}
function startEditingAccount(account) {
if (!account?.id) return;
editingAccountId = account.id;
if (dom.inputMail2925Email) dom.inputMail2925Email.value = String(account.email || '').trim();
if (dom.inputMail2925Password) dom.inputMail2925Password.value = String(account.password || '');
formController.setVisible(true, { focusField: false });
syncEditUi();
}
function stopEditingAccount(options = {}) {
const { clearForm = true } = options;
editingAccountId = '';
if (clearForm) {
clearMail2925Form();
}
syncEditUi();
}
function renderMail2925Accounts() {
if (!dom.mail2925AccountsList) return;
const latestState = state.getLatestState();
const accounts = getMail2925Accounts(latestState);
const currentId = getCurrentMail2925AccountId(latestState);
if (!accounts.length) {
dom.mail2925AccountsList.innerHTML = '<div class="hotmail-empty">还没有 2925 账号,先添加一条再使用。</div>';
updateMail2925ListViewport();
return;
}
dom.mail2925AccountsList.innerHTML = accounts.map((account) => {
const status = getStatusSnapshot(account);
const coolingDown = status.label === '冷却中';
return `
<div class="hotmail-account-item${account.id === currentId ? ' is-current' : ''}">
<div class="hotmail-account-top">
<div class="hotmail-account-title-row">
<div class="hotmail-account-email">${helpers.escapeHtml(account.email || '(未命名账号)')}</div>
<button
class="hotmail-copy-btn"
type="button"
data-account-action="copy-email"
data-account-id="${helpers.escapeHtml(account.id)}"
title="复制邮箱"
aria-label="复制邮箱 ${helpers.escapeHtml(account.email || '')}"
>${copyIcon}</button>
</div>
<span class="hotmail-status-chip ${helpers.escapeHtml(status.className)}">${helpers.escapeHtml(status.label)}</span>
</div>
<div class="hotmail-account-meta">
<span>密码:${account.password ? '已保存' : '未保存'}</span>
<span>上次登录:${helpers.escapeHtml(formatDateTime(account.lastLoginAt))}</span>
<span>上次使用:${helpers.escapeHtml(formatDateTime(account.lastUsedAt))}</span>
<span>上限记录:${helpers.escapeHtml(formatDateTime(account.lastLimitAt))}</span>
<span>恢复时间:${helpers.escapeHtml(formatDateTime(account.disabledUntil))}</span>
</div>
${account.lastError ? `<div class="hotmail-account-error">${helpers.escapeHtml(account.lastError)}</div>` : ''}
<div class="hotmail-account-actions">
<button class="btn btn-outline btn-sm" type="button" data-account-action="select" data-account-id="${helpers.escapeHtml(account.id)}">使用此账号</button>
<button class="btn btn-primary btn-sm" type="button" data-account-action="login" data-account-id="${helpers.escapeHtml(account.id)}">登录</button>
<button class="btn btn-outline btn-sm" type="button" data-account-action="edit" data-account-id="${helpers.escapeHtml(account.id)}">编辑</button>
<button class="btn btn-outline btn-sm" type="button" data-account-action="toggle-enabled" data-account-id="${helpers.escapeHtml(account.id)}">${account.enabled === false ? '启用' : '禁用'}</button>
${coolingDown ? `<button class="btn btn-outline btn-sm" type="button" data-account-action="clear-cooldown" data-account-id="${helpers.escapeHtml(account.id)}">清冷却</button>` : ''}
<button class="btn btn-ghost btn-sm" type="button" data-account-action="delete" data-account-id="${helpers.escapeHtml(account.id)}">删除</button>
</div>
</div>
`;
}).join('');
updateMail2925ListViewport();
}
async function handleAddMail2925Account() {
if (actionInFlight) return;
const email = String(dom.inputMail2925Email?.value || '').trim();
const password = String(dom.inputMail2925Password?.value || '');
if (!email) {
helpers.showToast('请先填写 2925 邮箱。', 'warn');
return;
}
if (!password) {
helpers.showToast('请先填写 2925 密码。', 'warn');
return;
}
const updatingExisting = Boolean(editingAccountId);
actionInFlight = true;
if (dom.btnAddMail2925Account) {
dom.btnAddMail2925Account.disabled = true;
}
try {
const response = await runtime.sendMessage({
type: 'UPSERT_MAIL2925_ACCOUNT',
source: 'sidepanel',
payload: {
...(editingAccountId ? { id: editingAccountId } : {}),
email,
password,
},
});
if (response?.error) {
throw new Error(response.error);
}
applyMail2925AccountMutation(response.account);
formController.setVisible(false, { clearForm: true });
helpers.showToast(
updatingExisting
? `已更新 2925 账号 ${email}`
: `已保存 2925 账号 ${email}`,
'success',
1800
);
} catch (err) {
helpers.showToast(`保存 2925 账号失败:${err.message}`, 'error');
} finally {
actionInFlight = false;
if (dom.btnAddMail2925Account) {
dom.btnAddMail2925Account.disabled = false;
}
}
}
async function handleImportMail2925Accounts() {
if (actionInFlight) return;
if (typeof mail2925Utils.parseMail2925ImportText !== 'function') {
helpers.showToast('2925 导入解析器未加载,请刷新扩展后重试。', 'error');
return;
}
const rawText = String(dom.inputMail2925Import?.value || '').trim();
if (!rawText) {
helpers.showToast('请先粘贴 2925 账号导入内容。', 'warn');
return;
}
const parsedAccounts = mail2925Utils.parseMail2925ImportText(rawText);
if (!parsedAccounts.length) {
helpers.showToast('没有解析到有效账号,请检查格式是否为 邮箱----密码。', 'error');
return;
}
actionInFlight = true;
if (dom.btnImportMail2925Accounts) {
dom.btnImportMail2925Accounts.disabled = true;
}
try {
for (const account of parsedAccounts) {
const response = await runtime.sendMessage({
type: 'UPSERT_MAIL2925_ACCOUNT',
source: 'sidepanel',
payload: account,
});
if (response?.error) {
throw new Error(response.error);
}
}
if (dom.inputMail2925Import) {
dom.inputMail2925Import.value = '';
}
helpers.showToast(`已导入 ${parsedAccounts.length} 条 2925 账号`, 'success', 2200);
} catch (err) {
helpers.showToast(`批量导入 2925 账号失败:${err.message}`, 'error');
} finally {
actionInFlight = false;
if (dom.btnImportMail2925Accounts) {
dom.btnImportMail2925Accounts.disabled = false;
}
}
}
async function deleteAllMail2925Accounts() {
const accounts = getMail2925Accounts();
if (!accounts.length) {
helpers.showToast('没有可删除的 2925 账号。', 'warn');
return;
}
const confirmed = await helpers.openConfirmModal({
title: '全部删除 2925 账号',
message: `确认删除当前全部 ${accounts.length} 个 2925 账号吗?`,
confirmLabel: '确认全部删除',
confirmVariant: 'btn-danger',
});
if (!confirmed) {
return;
}
const response = await runtime.sendMessage({
type: 'DELETE_MAIL2925_ACCOUNTS',
source: 'sidepanel',
payload: { mode: 'all' },
});
if (response?.error) {
throw new Error(response.error);
}
state.syncLatestState({
mail2925Accounts: [],
currentMail2925AccountId: null,
});
formController.setVisible(false, { clearForm: true });
refreshManagedAliasBaseEmail();
renderMail2925Accounts();
helpers.showToast(`已删除全部 ${response.deletedCount || 0} 个 2925 账号`, 'success', 2200);
}
async function handleAccountListClick(event) {
const actionButton = event.target.closest('[data-account-action]');
if (!actionButton || actionInFlight) {
return;
}
const accountId = String(actionButton.dataset.accountId || '');
const action = String(actionButton.dataset.accountAction || '');
if (!accountId || !action) {
return;
}
const targetAccount = getMail2925Accounts().find((account) => account.id === accountId) || null;
actionInFlight = true;
actionButton.disabled = true;
try {
if (action === 'copy-email') {
if (!targetAccount?.email) throw new Error('未找到可复制的 2925 邮箱。');
await helpers.copyTextToClipboard(targetAccount.email);
helpers.showToast(`已复制 ${targetAccount.email}`, 'success', 1800);
return;
}
if (action === 'select') {
const response = await runtime.sendMessage({
type: 'SELECT_MAIL2925_ACCOUNT',
source: 'sidepanel',
payload: { accountId },
});
if (response?.error) throw new Error(response.error);
state.syncLatestState({ currentMail2925AccountId: response.account.id });
refreshManagedAliasBaseEmail();
renderMail2925Accounts();
helpers.showToast(`已切换当前 2925 账号为 ${response.account.email}`, 'success', 2000);
return;
}
if (action === 'login') {
const response = await runtime.sendMessage({
type: 'LOGIN_MAIL2925_ACCOUNT',
source: 'sidepanel',
payload: {
accountId,
forceRelogin: true,
},
});
if (response?.error) throw new Error(response.error);
state.syncLatestState({ currentMail2925AccountId: response.account.id });
refreshManagedAliasBaseEmail();
renderMail2925Accounts();
helpers.showToast(`已使用 ${response.account.email} 登录 2925 邮箱`, 'success', 2200);
return;
}
if (action === 'edit') {
if (!targetAccount) throw new Error('未找到目标 2925 账号。');
startEditingAccount(targetAccount);
helpers.showToast(`已载入 ${targetAccount.email},修改后点“保存修改”即可`, 'info', 1800);
return;
}
if (action === 'toggle-enabled') {
if (!targetAccount) throw new Error('未找到目标 2925 账号。');
const response = await runtime.sendMessage({
type: 'PATCH_MAIL2925_ACCOUNT',
source: 'sidepanel',
payload: {
accountId,
updates: {
enabled: targetAccount.enabled === false,
},
},
});
if (response?.error) throw new Error(response.error);
applyMail2925AccountMutation(response.account);
helpers.showToast(`2925 账号 ${response.account.email}${response.account.enabled === false ? '禁用' : '启用'}`, 'success', 2200);
return;
}
if (action === 'clear-cooldown') {
const response = await runtime.sendMessage({
type: 'PATCH_MAIL2925_ACCOUNT',
source: 'sidepanel',
payload: {
accountId,
updates: {
disabledUntil: 0,
lastError: '',
},
},
});
if (response?.error) throw new Error(response.error);
applyMail2925AccountMutation(response.account);
helpers.showToast(`2925 账号 ${response.account.email} 已清除冷却`, 'success', 2200);
return;
}
if (action === 'delete') {
const confirmed = await helpers.openConfirmModal({
title: '删除 2925 账号',
message: '确认删除这个 2925 账号吗?',
confirmLabel: '确认删除',
confirmVariant: 'btn-danger',
});
if (!confirmed) {
return;
}
const response = await runtime.sendMessage({
type: 'DELETE_MAIL2925_ACCOUNT',
source: 'sidepanel',
payload: { accountId },
});
if (response?.error) throw new Error(response.error);
const nextAccounts = getMail2925Accounts().filter((account) => account.id !== accountId);
const nextState = { mail2925Accounts: nextAccounts };
if (getCurrentMail2925AccountId() === accountId) {
nextState.currentMail2925AccountId = null;
}
state.syncLatestState(nextState);
if (editingAccountId === accountId) {
formController.setVisible(false, { clearForm: true });
}
refreshManagedAliasBaseEmail();
renderMail2925Accounts();
helpers.showToast('2925 账号已删除', 'success', 1800);
}
} catch (err) {
helpers.showToast(err.message, 'error');
} finally {
actionInFlight = false;
actionButton.disabled = false;
}
}
function bindMail2925Events() {
dom.btnToggleMail2925List?.addEventListener('click', () => {
setMail2925ListExpanded(!listExpanded);
});
dom.btnToggleMail2925Form?.addEventListener('click', () => {
if (formController.isVisible()) {
formController.setVisible(false, { clearForm: true });
return;
}
formController.setVisible(true, { clearForm: !editingAccountId, focusField: true });
});
dom.btnDeleteAllMail2925Accounts?.addEventListener('click', async () => {
if (actionInFlight) return;
actionInFlight = true;
try {
await deleteAllMail2925Accounts();
} catch (err) {
helpers.showToast(err.message, 'error');
} finally {
actionInFlight = false;
updateMail2925ListViewport();
}
});
dom.btnAddMail2925Account?.addEventListener('click', handleAddMail2925Account);
dom.btnImportMail2925Accounts?.addEventListener('click', handleImportMail2925Accounts);
dom.mail2925AccountsList?.addEventListener('click', handleAccountListClick);
syncEditUi();
formController.sync();
}
return {
bindMail2925Events,
initMail2925ListExpandedState,
renderMail2925Accounts,
};
}
globalScope.SidepanelMail2925Manager = {
createMail2925Manager,
};
})(window);
+267 -61
View File
@@ -100,7 +100,9 @@ body {
header {
display: flex;
justify-content: space-between;
align-items: center;
align-items: flex-start;
flex-wrap: wrap;
gap: 10px;
position: sticky;
top: 0;
z-index: 200;
@@ -228,12 +230,91 @@ header {
gap: 4px;
flex-shrink: 0;
flex-wrap: wrap;
justify-content: flex-end;
}
.btn-contribution-mode {
padding-inline: 10px;
}
.btn-contribution-mode.is-active {
border-color: color-mix(in srgb, var(--orange) 58%, var(--border));
background: color-mix(in srgb, var(--orange) 14%, var(--bg-base));
color: var(--orange);
}
.contribution-update-layer {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1400;
}
.contribution-update-hint {
--contribution-update-arrow-left: 20px;
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: flex-start;
gap: 8px;
max-width: min(220px, calc(100vw - 24px));
padding: 8px 10px;
background: color-mix(in srgb, var(--amber) 10%, var(--bg-base));
border: 1px solid color-mix(in srgb, var(--amber) 34%, var(--border));
border-radius: 10px;
box-shadow: var(--shadow-sm);
pointer-events: auto;
}
.contribution-update-hint::before {
content: '';
position: absolute;
top: -7px;
left: calc(var(--contribution-update-arrow-left) - 6px);
width: 12px;
height: 12px;
background: inherit;
border-top: inherit;
border-left: inherit;
transform: rotate(45deg);
}
.contribution-update-hint-text {
font-size: 12px;
line-height: 1.45;
color: var(--text-primary);
}
.contribution-update-hint-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
padding: 0;
border: none;
border-radius: 999px;
background: transparent;
color: var(--text-secondary);
font: inherit;
font-size: 14px;
line-height: 1;
cursor: pointer;
flex-shrink: 0;
transition: background var(--transition), color var(--transition);
}
.contribution-update-hint-close:hover {
background: color-mix(in srgb, var(--amber) 16%, transparent);
color: var(--text-primary);
}
.contribution-update-hint-close:focus-visible {
outline: 2px solid color-mix(in srgb, var(--amber) 34%, transparent);
outline-offset: 1px;
}
/* ============================================================
Theme Toggle
============================================================ */
@@ -541,6 +622,108 @@ header {
pointer-events: none;
}
.is-contribution-hidden {
display: none !important;
}
.contribution-mode-panel {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px;
border: 1px solid color-mix(in srgb, var(--orange) 28%, var(--border));
border-radius: var(--radius-md);
background:
linear-gradient(180deg,
color-mix(in srgb, var(--orange) 10%, var(--bg-base)),
color-mix(in srgb, var(--bg-surface) 96%, var(--bg-base))
);
}
.contribution-mode-panel[hidden] {
display: none !important;
}
.contribution-mode-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.contribution-mode-badge {
padding: 4px 8px;
border-radius: 999px;
background: color-mix(in srgb, var(--orange) 16%, var(--bg-base));
color: var(--orange);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
}
.contribution-mode-text {
margin: 0;
font-size: 13px;
line-height: 1.6;
color: var(--text-secondary);
}
.contribution-mode-status-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.contribution-mode-status-card {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--bg-base) 92%, var(--orange-soft));
}
.contribution-mode-status-label {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
color: var(--text-muted);
}
.contribution-mode-status-value {
font-size: 13px;
font-weight: 600;
line-height: 1.5;
color: var(--text-primary);
}
.contribution-mode-summary {
padding: 10px 12px;
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--bg-base) 88%, var(--orange-soft));
color: var(--text-secondary);
font-size: 12px;
line-height: 1.6;
}
.contribution-mode-actions {
display: flex;
flex-direction: column;
gap: 8px;
}
.contribution-mode-actions .btn {
width: 100%;
justify-content: center;
}
@media (max-width: 540px) {
.contribution-mode-status-grid {
grid-template-columns: 1fr;
}
}
.section-mini-header {
display: flex;
align-items: center;
@@ -586,6 +769,21 @@ header {
min-width: 0;
}
.mail2925-base-inline {
gap: 10px;
}
.mail2925-base-input,
.mail2925-pool-select {
flex: 1;
min-width: 0;
}
.mail2925-pool-toggle {
flex: 0 0 auto;
white-space: nowrap;
}
.data-value-actions {
justify-content: space-between;
}
@@ -608,6 +806,17 @@ header {
flex-shrink: 0;
}
#row-codex2api-url .data-label,
#row-codex2api-admin-key .data-label {
width: 76px;
}
#row-codex2api-url .data-label {
font-size: 10px;
letter-spacing: 0.02em;
text-transform: none;
}
.data-value {
font-size: 13px;
color: var(--text-muted);
@@ -916,6 +1125,20 @@ header {
visibility: hidden;
}
.account-pool-form-shell {
display: flex;
flex-direction: column;
gap: 10px;
}
.account-pool-actions-inline {
width: 100%;
}
.account-pool-import-action {
margin-left: auto;
}
.hotmail-import-row {
align-items: flex-start;
}
@@ -938,7 +1161,7 @@ header {
}
.hotmail-list-shell.is-collapsed {
max-height: 236px;
max-height: 176px;
}
.hotmail-list-shell.is-expanded {
@@ -1284,36 +1507,51 @@ header {
flex-shrink: 0;
}
.fallback-inline {
justify-content: space-between;
.setting-pair {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex: 1;
min-width: 0;
flex-wrap: nowrap;
}
.fallback-thread-interval {
margin-left: auto;
}
.timing-field {
.setting-group {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.timing-field-labeled {
min-width: 196px;
justify-content: flex-end;
.setting-group-primary {
flex: 1;
min-width: 112px;
}
.timing-field-right {
.setting-group-secondary {
margin-left: auto;
}
.setting-inline-right {
min-width: 196px;
justify-content: flex-end;
}
.setting-caption {
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
flex: 0 0 auto;
white-space: nowrap;
min-width: 56px;
text-align: right;
}
.setting-controls {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.toggle-switch {
position: relative;
display: inline-flex;
@@ -1371,51 +1609,6 @@ header {
opacity: 0.55;
}
.auto-delay-inline {
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: nowrap;
}
.auto-delay-side {
display: flex;
align-items: center;
gap: 12px;
flex: 0 0 auto;
min-width: 0;
}
.auto-delay-side-right {
flex: 0 0 auto;
}
.auto-delay-check {
flex: 0 0 auto;
align-items: center;
}
.auto-delay-check span {
white-space: nowrap;
}
.auto-delay-caption {
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
flex: 0 0 auto;
white-space: nowrap;
min-width: 56px;
text-align: right;
}
.auto-delay-controls {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.auto-delay-input {
width: 72px;
flex: 0 0 auto;
@@ -2211,6 +2404,19 @@ header {
line-height: 1.55;
color: var(--text-secondary);
margin-bottom: 14px;
white-space: pre-line;
}
.modal-message a,
.modal-alert a {
color: var(--blue);
text-decoration: underline;
text-underline-offset: 2px;
}
.modal-message a:hover,
.modal-alert a:hover {
color: var(--cyan);
}
.modal-alert {
+235 -88
View File
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多页面自动化面板</title>
<title>codex-oauth-automation-extension</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
@@ -35,8 +35,8 @@
<div class="header-btns">
<div class="run-group">
<button id="btn-contribution-mode" class="btn btn-outline btn-sm btn-contribution-mode" type="button"
title="打开账号贡献上传页">贡献</button>
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="运行次数" />
aria-pressed="false" title="进入贡献模式并打开上传页">贡献/使用</button>
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" title="运行次数" />
<button id="btn-auto-run" class="btn btn-success" title="自动执行全部步骤">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3" />
@@ -81,6 +81,16 @@
</div>
</header>
<div id="contribution-update-layer" class="contribution-update-layer" hidden>
<div id="contribution-update-hint" class="contribution-update-hint" aria-live="polite" hidden>
<p id="contribution-update-hint-text" class="contribution-update-hint-text">
公告 / 使用教程有更新了,可点上方“贡献/使用”查看。
</p>
<button id="btn-dismiss-contribution-update-hint" class="contribution-update-hint-close" type="button"
aria-label="关闭更新提示" title="关闭更新提示">×</button>
</div>
</div>
<section id="update-section" hidden>
<div class="update-card">
<div class="update-card-header">
@@ -102,8 +112,40 @@
<select id="select-panel-mode" class="data-select">
<option value="cpa">CPA 面板</option>
<option value="sub2api">SUB2API</option>
<option value="codex2api">Codex2API</option>
</select>
</div>
<div id="contribution-mode-panel" class="contribution-mode-panel" hidden>
<div class="contribution-mode-panel-header">
<span class="section-label">贡献模式</span>
<span class="contribution-mode-badge">CPA</span>
</div>
<p id="contribution-mode-text" class="contribution-mode-text">当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,无需手动复制。</p>
<div class="data-row contribution-mode-field">
<span class="data-label">贡献昵称</span>
<input type="text" id="input-contribution-nickname" class="data-input" placeholder="可留空,将显示为匿名贡献者" />
</div>
<div class="data-row contribution-mode-field">
<span class="data-label">QQ</span>
<input type="text" id="input-contribution-qq" class="data-input" inputmode="numeric" placeholder="可留空,只能填写数字" />
</div>
<div class="contribution-mode-status-grid">
<div class="contribution-mode-status-card">
<span class="contribution-mode-status-label">OAUTH</span>
<span id="contribution-oauth-status" class="contribution-mode-status-value">未生成登录地址</span>
</div>
<div class="contribution-mode-status-card">
<span class="contribution-mode-status-label">回调</span>
<span id="contribution-callback-status" class="contribution-mode-status-value">等待回调</span>
</div>
</div>
<div id="contribution-mode-summary" class="contribution-mode-summary">等待开始贡献</div>
<div class="contribution-mode-actions">
<button id="btn-start-contribution" class="btn btn-primary btn-sm" type="button">开始贡献</button>
<button id="btn-open-contribution-upload" class="btn btn-outline btn-sm" type="button">已有认证文件?前往上传</button>
<button id="btn-exit-contribution-mode" class="btn btn-ghost btn-sm" type="button">退出贡献模式</button>
</div>
</div>
<div class="data-row" id="row-vps-url">
<span class="data-label">CPA</span>
<div class="input-with-icon">
@@ -123,10 +165,10 @@
</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 第 10 步策略">
<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">跳过第10步</button>
<span class="data-label">回调方式</span>
<div id="local-cpa-step9-mode-group" class="choice-group" role="group" aria-label="回调方式">
<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">本地部署</button>
</div>
</div>
<div class="data-row" id="row-sub2api-url" style="display:none;">
@@ -151,7 +193,17 @@
<input type="text" id="input-sub2api-default-proxy" class="data-input"
placeholder="留空则不使用代理;或填写代理名称 / ID" />
</div>
<div class="data-row">
<div class="data-row" id="row-codex2api-url" style="display:none;">
<span class="data-label">Codex2API</span>
<input type="text" id="input-codex2api-url" class="data-input"
placeholder="http://localhost:8080/admin/accounts" />
</div>
<div class="data-row" id="row-codex2api-admin-key" style="display:none;">
<span class="data-label">管理密钥</span>
<input type="password" id="input-codex2api-admin-key" class="data-input"
placeholder="请输入 Codex2API Admin Secret" />
</div>
<div class="data-row" id="row-custom-password">
<span class="data-label">账户密码</span>
<div class="input-with-icon">
<input type="password" id="input-password" class="data-input data-input-with-icon"
@@ -195,31 +247,6 @@
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
</select>
</div>
<div class="data-row" id="row-temp-email-base-url" style="display:none;">
<span class="data-label">Temp API</span>
<input type="text" id="input-temp-email-base-url" class="data-input" placeholder="https://your-worker-domain" />
</div>
<div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
<span class="data-label">Admin Auth</span>
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon" placeholder="Cloudflare Temp Email admin password" />
</div>
<div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
<span class="data-label">Custom Auth</span>
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon" placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
</div>
<div class="data-row" id="row-temp-email-receive-mailbox" style="display:none;">
<span class="data-label">邮件接收</span>
<input type="text" id="input-temp-email-receive-mailbox" class="data-input" placeholder="用于接收转发邮件的邮箱,例如 1@email.example.com" />
</div>
<div class="data-row" id="row-temp-email-domain" style="display:none;">
<span class="data-label">Temp 域名</span>
<div class="data-inline">
<select id="select-temp-email-domain" class="data-select"></select>
<input type="text" id="input-temp-email-domain" class="data-input" placeholder="例如 mail.example.com"
style="display:none;" />
<button id="btn-temp-email-domain-mode" class="btn btn-outline btn-sm" type="button">添加</button>
</div>
</div>
<div class="data-row" id="row-cf-domain" style="display:none;">
<span class="data-label">CF 域名</span>
<div class="data-inline">
@@ -229,9 +256,26 @@
<button id="btn-cf-domain-mode" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
</div>
</div>
<div class="data-row" id="row-mail2925-pool-settings" style="display:none;">
<span class="data-label">2925 号池</span>
<div class="data-inline mail2925-base-inline">
<label class="toggle-switch mail2925-pool-toggle" id="label-mail2925-use-account-pool" for="input-mail2925-use-account-pool" style="display:none;" title="开启后启用 2925 账号池">
<input type="checkbox" id="input-mail2925-use-account-pool" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
<span>号池</span>
</label>
<select id="select-mail2925-pool-account" class="data-select mail2925-pool-select" style="display:none;">
<option value="">请选择号池邮箱</option>
</select>
</div>
</div>
<div class="data-row" id="row-email-prefix" style="display:none;">
<span class="data-label" id="label-email-prefix">别名基邮箱</span>
<input type="text" id="input-email-prefix" class="data-input" placeholder="例如 yourname@example.com" />
<div class="data-inline">
<input type="text" id="input-email-prefix" class="data-input mail2925-base-input" placeholder="例如 yourname@example.com" />
</div>
</div>
<div class="data-row" id="row-inbucket-host" style="display:none;">
<span class="data-label">Inbucket</span>
@@ -250,23 +294,23 @@
</div>
<div class="data-row">
<span class="data-label">延迟</span>
<div class="data-inline auto-delay-inline">
<div class="auto-delay-side auto-delay-side-left">
<div class="data-inline setting-pair">
<div class="setting-group setting-group-primary">
<label class="toggle-switch" for="input-auto-delay-enabled">
<input type="checkbox" id="input-auto-delay-enabled" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
<div class="auto-delay-controls">
<div class="setting-controls">
<input type="number" id="input-auto-delay-minutes" class="data-input auto-delay-input" value="30" min="1"
max="1440" step="1" title="启动前倒计时分钟数" />
<span class="data-unit">分钟</span>
</div>
</div>
<div class="timing-field timing-field-labeled timing-field-right auto-delay-side auto-delay-side-right">
<span class="auto-delay-caption">步间间隔</span>
<div class="auto-delay-controls">
<div class="setting-group setting-group-secondary">
<span class="setting-caption">步间间隔</span>
<div class="setting-controls">
<input type="number" id="input-auto-step-delay-seconds" class="data-input auto-delay-input" value=""
min="0" max="600" step="1" title="步间延迟秒数,0 或留空则不延迟" />
<span class="data-unit"></span>
@@ -276,16 +320,18 @@
</div>
<div class="data-row">
<span class="data-label">自动重试</span>
<div class="data-inline fallback-inline">
<label class="toggle-switch" for="input-auto-skip-failures">
<input type="checkbox" id="input-auto-skip-failures" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
<div class="timing-field timing-field-labeled timing-field-right fallback-thread-interval">
<span class="auto-delay-caption">线程间隔</span>
<div class="auto-delay-controls">
<div class="data-inline setting-pair">
<div class="setting-group setting-group-primary">
<label class="toggle-switch" for="input-auto-skip-failures">
<input type="checkbox" id="input-auto-skip-failures" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
</div>
<div class="setting-group setting-group-secondary">
<span class="setting-caption">线程间隔</span>
<div class="setting-controls">
<input type="number" id="input-auto-skip-failures-thread-interval-minutes" class="data-input auto-delay-input" value="0"
min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" />
<span class="data-unit">分钟</span>
@@ -293,18 +339,20 @@
</div>
</div>
</div>
<div class="data-row">
<div class="data-row" id="row-account-run-history-text-enabled">
<span class="data-label">本地同步</span>
<div class="data-inline auto-delay-inline">
<label class="toggle-switch" for="input-account-run-history-text-enabled">
<input type="checkbox" id="input-account-run-history-text-enabled" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
<div class="timing-field timing-field-right setting-inline-right">
<span class="auto-delay-caption">验证码重发</span>
<div class="auto-delay-controls">
<div class="data-inline setting-pair">
<div class="setting-group setting-group-primary">
<label class="toggle-switch" for="input-account-run-history-text-enabled">
<input type="checkbox" id="input-account-run-history-text-enabled" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
</div>
<div class="setting-group setting-group-secondary">
<span class="setting-caption">验证码重发</span>
<div class="setting-controls">
<input type="number" id="input-verification-resend-count" class="data-input auto-delay-input" value="4"
min="0" max="20" step="1" title="自动点击重新发送验证码的次数" />
<span class="data-unit"></span>
@@ -328,12 +376,63 @@
</div>
</div>
</div>
<div id="cloudflare-temp-email-section" class="data-card hotmail-card" style="display:none;">
<div class="section-mini-header">
<div class="section-mini-copy">
<span class="section-label">Cloudflare Temp Email</span>
<span class="data-value">用于生成邮箱或接收转发邮件</span>
</div>
<div class="section-mini-actions">
<button id="btn-cloudflare-temp-email-usage-guide" class="btn btn-ghost btn-xs" type="button">使用教程</button>
<button id="btn-cloudflare-temp-email-github" class="btn btn-ghost btn-xs" type="button">GitHub</button>
</div>
</div>
<div class="data-row" id="row-temp-email-base-url" style="display:none;">
<span class="data-label">Temp API</span>
<input type="text" id="input-temp-email-base-url" class="data-input" placeholder="https://your-worker-domain" />
</div>
<div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
<span class="data-label">Admin Auth</span>
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon" placeholder="Cloudflare Temp Email admin password" />
</div>
<div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
<span class="data-label">Custom Auth</span>
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon" placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
</div>
<div class="data-row" id="row-temp-email-receive-mailbox" style="display:none;">
<span class="data-label">邮件接收</span>
<input type="text" id="input-temp-email-receive-mailbox" class="data-input" placeholder="用于接收转发邮件的邮箱,例如 1@email.example.com" />
</div>
<div class="data-row" id="row-temp-email-random-subdomain-toggle" style="display:none;">
<span class="data-label">随机子域</span>
<div class="data-inline">
<label class="toggle-switch" for="input-temp-email-use-random-subdomain" title="依赖后端 RANDOM_SUBDOMAIN_DOMAINS 配置">
<input type="checkbox" id="input-temp-email-use-random-subdomain" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
<span>启用</span>
</label>
<span class="setting-caption">依赖后端 RANDOM_SUBDOMAIN_DOMAINS</span>
</div>
</div>
<div class="data-row" id="row-temp-email-domain" style="display:none;">
<span class="data-label">Temp 域名</span>
<div class="data-inline">
<select id="select-temp-email-domain" class="data-select"></select>
<input type="text" id="input-temp-email-domain" class="data-input" placeholder="例如 mail.example.com"
style="display:none;" />
<button id="btn-temp-email-domain-mode" class="btn btn-outline btn-sm" type="button">添加</button>
</div>
</div>
</div>
<div id="hotmail-section" class="data-card hotmail-card" style="display:none;">
<div class="section-mini-header">
<div class="section-mini-copy">
<span class="section-label">Hotmail 账号池</span>
</div>
<div class="section-mini-actions">
<button id="btn-toggle-hotmail-form" class="btn btn-outline btn-xs" type="button" aria-expanded="false">添加账号</button>
<button id="btn-hotmail-usage-guide" class="btn btn-ghost btn-xs" type="button">使用教程</button>
<button id="btn-clear-used-hotmail-accounts" class="btn btn-ghost btn-xs" type="button">清空已用</button>
<button id="btn-delete-all-hotmail-accounts" class="btn btn-ghost btn-xs" type="button">全部删除</button>
@@ -356,39 +455,82 @@
<span class="data-label">本地助手</span>
<input type="text" id="input-hotmail-local-base-url" class="data-input mono" placeholder="http://127.0.0.1:17373" />
</div>
<div class="data-row">
<span class="data-label">邮箱</span>
<input type="text" id="input-hotmail-email" class="data-input" placeholder="name@hotmail.com" />
</div>
<div class="data-row">
<span class="data-label">客户端 ID</span>
<input type="text" id="input-hotmail-client-id" class="data-input mono" placeholder="微软应用客户端 ID" />
</div>
<div class="data-row">
<span class="data-label">邮箱密码</span>
<input type="password" id="input-hotmail-password" class="data-input" placeholder="可选,仅用于记录" />
</div>
<div class="data-row">
<span class="data-label">刷新令牌</span>
<input type="password" id="input-hotmail-refresh-token" class="data-input mono"
placeholder="必填,粘贴刷新令牌(refresh token" />
</div>
<div class="data-row hotmail-actions-row">
<span class="data-label"></span>
<button id="btn-add-hotmail-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
</div>
<div class="data-row hotmail-import-row">
<span class="data-label">批量导入</span>
<div class="hotmail-import-box">
<textarea id="input-hotmail-import" class="data-textarea mono"
placeholder="账号----密码----客户端ID----刷新令牌&#10;name@hotmail.com----password----client-id----refresh-token"></textarea>
<button id="btn-import-hotmail-accounts" class="btn btn-outline btn-sm" type="button">导入</button>
<div id="hotmail-form-shell" class="account-pool-form-shell" hidden>
<div class="data-row">
<span class="data-label">邮箱</span>
<input type="text" id="input-hotmail-email" class="data-input" placeholder="name@hotmail.com" />
</div>
<div class="data-row">
<span class="data-label">客户端 ID</span>
<input type="text" id="input-hotmail-client-id" class="data-input mono" placeholder="微软应用客户端 ID" />
</div>
<div class="data-row">
<span class="data-label">邮箱密码</span>
<input type="password" id="input-hotmail-password" class="data-input" placeholder="可选,仅用于记录" />
</div>
<div class="data-row">
<span class="data-label">刷新令牌</span>
<input type="password" id="input-hotmail-refresh-token" class="data-input mono"
placeholder="必填,粘贴刷新令牌(refresh token" />
</div>
<div class="data-row hotmail-actions-row">
<span class="data-label"></span>
<div class="data-inline account-pool-actions-inline">
<button id="btn-add-hotmail-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
<button id="btn-import-hotmail-accounts" class="btn btn-outline btn-sm account-pool-import-action" type="button">批量导入</button>
</div>
</div>
<div class="data-row hotmail-import-row">
<span class="data-label">批量导入</span>
<div class="hotmail-import-box">
<textarea id="input-hotmail-import" class="data-textarea mono"
placeholder="账号----密码----客户端ID----刷新令牌&#10;name@hotmail.com----password----client-id----refresh-token"></textarea>
</div>
</div>
</div>
<div id="hotmail-list-shell" class="hotmail-list-shell is-collapsed">
<div id="hotmail-accounts-list" class="hotmail-accounts-list"></div>
</div>
</div>
<div id="mail2925-section" class="data-card hotmail-card" style="display:none;">
<div class="section-mini-header">
<div class="section-mini-copy">
<span class="section-label">2925 账号池</span>
</div>
<div class="section-mini-actions">
<button id="btn-toggle-mail2925-form" class="btn btn-outline btn-xs" type="button" aria-expanded="false">添加账号</button>
<button id="btn-delete-all-mail2925-accounts" class="btn btn-ghost btn-xs" type="button">全部删除</button>
<button id="btn-toggle-mail2925-list" class="btn btn-ghost btn-xs" type="button" aria-expanded="false">展开列表</button>
</div>
</div>
<div id="mail2925-form-shell" class="account-pool-form-shell" hidden>
<div class="data-row">
<span class="data-label">邮箱</span>
<input type="text" id="input-mail2925-email" class="data-input" placeholder="name@2925.com" />
</div>
<div class="data-row">
<span class="data-label">密码</span>
<input type="password" id="input-mail2925-password" class="data-input" placeholder="2925 登录密码" />
</div>
<div class="data-row hotmail-actions-row">
<span class="data-label"></span>
<div class="data-inline account-pool-actions-inline">
<button id="btn-add-mail2925-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
<button id="btn-import-mail2925-accounts" class="btn btn-outline btn-sm account-pool-import-action" type="button">批量导入</button>
</div>
</div>
<div class="data-row hotmail-import-row">
<span class="data-label">批量导入</span>
<div class="hotmail-import-box">
<textarea id="input-mail2925-import" class="data-textarea mono"
placeholder="邮箱----密码&#10;name@2925.com----password"></textarea>
</div>
</div>
</div>
<div id="mail2925-list-shell" class="hotmail-list-shell is-collapsed">
<div id="mail2925-accounts-list" class="hotmail-accounts-list"></div>
</div>
</div>
<div id="luckmail-section" class="data-card luckmail-card" style="display:none;">
<div class="section-mini-header">
<div class="section-mini-copy">
@@ -621,14 +763,19 @@
<div id="toast-container"></div>
<input id="input-import-settings-file" type="file" accept=".json,application/json" hidden />
<script src="../managed-alias-utils.js"></script>
<script src="../mail2925-utils.js"></script>
<script src="../icloud-utils.js"></script>
<script src="../hotmail-utils.js"></script>
<script src="../luckmail-utils.js"></script>
<script src="../data/step-definitions.js"></script>
<script src="update-service.js"></script>
<script src="contribution-content-update-service.js"></script>
<script src="account-pool-ui.js"></script>
<script src="hotmail-manager.js"></script>
<script src="mail-2925-manager.js"></script>
<script src="icloud-manager.js"></script>
<script src="luckmail-manager.js"></script>
<script src="contribution-mode.js"></script>
<script src="account-records-manager.js"></script>
<script src="sidepanel.js"></script>
</body>
+846 -108
View File
File diff suppressed because it is too large Load Diff