import GuJumpgate snapshot from GitHub

This commit is contained in:
2026-05-20 20:33:51 +08:00
commit f8903dcd14
335 changed files with 179790 additions and 0 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);
+843
View File
@@ -0,0 +1,843 @@
(function attachSidepanelAccountRecordsManager(globalScope) {
function createAccountRecordsManager(context = {}) {
const {
state,
dom,
helpers,
runtime,
constants = {},
} = context;
const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai';
const pageSize = Math.max(1, Math.floor(Number(constants.pageSize) || 10));
const FILTER_CONFIG = {
all: {
label: '总',
className: '',
matches: () => true,
metaLabel: '全部',
},
success: {
label: '成',
className: 'is-success',
matches: (record) => getRecordDisplayStatus(record) === 'success',
metaLabel: '成功',
},
running: {
label: '运行',
className: 'is-running',
matches: (record) => getRecordDisplayStatus(record) === 'running',
metaLabel: '运行中',
},
failed: {
label: '失',
className: 'is-failed',
matches: (record) => getRecordDisplayStatus(record) === 'failed',
metaLabel: '失败',
},
stopped: {
label: '停',
className: 'is-stopped',
matches: (record) => getRecordDisplayStatus(record) === 'stopped',
metaLabel: '停止',
},
retry: {
label: '重试',
className: 'is-retry',
matches: (record) => normalizeRetryCount(record.retryCount) > 0,
metaLabel: '重试',
},
};
let currentPage = 1;
let activeFilter = 'all';
let selectionMode = false;
let eventsBound = false;
const selectedRecordIds = new Set();
function escapeHtml(value) {
if (typeof helpers.escapeHtml === 'function') {
return helpers.escapeHtml(String(value || ''));
}
return String(value || '');
}
function normalizeTimestamp(value) {
const timestamp = Date.parse(String(value || ''));
return Number.isFinite(timestamp) ? timestamp : 0;
}
function normalizeRetryCount(value) {
const count = Math.floor(Number(value) || 0);
return count > 0 ? count : 0;
}
function buildRecordId(record = {}) {
const rawRecordId = String(record.recordId || '').trim();
if (rawRecordId) {
return rawRecordId.toLowerCase();
}
const rawIdentifierType = String(record.accountIdentifierType || '').trim().toLowerCase();
const hasPhoneOnlyIdentifier = !record.email && (
record.phoneNumber
|| record.phone
|| record.number
|| (record.accountIdentifier && !/@/.test(String(record.accountIdentifier || '')))
);
const identifierType = rawIdentifierType === 'phone'
|| (!rawIdentifierType && hasPhoneOnlyIdentifier)
? 'phone'
: 'email';
const identifier = String(
record.accountIdentifier
|| (identifierType === 'phone' ? (record.phoneNumber || record.phone || record.number || '') : (record.email || ''))
|| ''
).trim();
if (!identifier) {
return '';
}
return identifierType === 'phone'
? `phone:${identifier.toLowerCase()}`
: identifier.toLowerCase();
}
function getRecordDisplayStatus(record = {}) {
return String(record.displayStatus || record.finalStatus || '').trim().toLowerCase();
}
function isAutoRunRecordDisplayRunning(currentState = {}) {
const phase = String(currentState.autoRunPhase || '').trim().toLowerCase();
return Boolean(currentState.autoRunning)
&& ['running', 'waiting_step', 'waiting_email', 'retrying'].includes(phase);
}
function buildCurrentAccountRecordId(currentState = {}) {
const accountIdentifierType = String(currentState.accountIdentifierType || '').trim().toLowerCase();
const email = String(currentState.email || '').trim();
const phoneNumber = String(
currentState.signupPhoneNumber
|| currentState.phoneNumber
|| currentState.phone
|| ''
).trim();
const accountIdentifier = String(
currentState.accountIdentifier
|| (accountIdentifierType === 'phone' ? phoneNumber : email)
|| ''
).trim();
return buildRecordId({
accountIdentifierType,
accountIdentifier,
email,
phoneNumber,
});
}
function applyRunningDisplayState(record = {}, currentState = {}) {
if (!isAutoRunRecordDisplayRunning(currentState)) {
return record;
}
if (getRecordDisplayStatus(record) === 'success') {
return record;
}
const currentRecordId = buildCurrentAccountRecordId(currentState);
if (!currentRecordId || buildRecordId(record) !== currentRecordId) {
return record;
}
return {
...record,
displayStatus: 'running',
displaySummary: '正在运行',
};
}
function getRecordIdentifierType(record = {}) {
const rawType = String(record.accountIdentifierType || '').trim().toLowerCase();
if (rawType === 'phone') {
return 'phone';
}
if (rawType === 'email') {
return 'email';
}
if (!record.email && (record.phoneNumber || record.phone || record.number)) {
return 'phone';
}
if (!record.email && record.accountIdentifier && !/@/.test(String(record.accountIdentifier || ''))) {
return 'phone';
}
return 'email';
}
function getRecordEmail(record = {}) {
const identifierType = getRecordIdentifierType(record);
return String(
record.email
|| (identifierType === 'email' ? record.accountIdentifier : '')
|| ''
).trim();
}
function getRecordPhoneNumber(record = {}) {
const identifierType = getRecordIdentifierType(record);
return String(
record.phoneNumber
|| record.phone
|| record.number
|| (identifierType === 'phone' ? record.accountIdentifier : '')
|| ''
).trim();
}
function getRecordPrimaryIdentifier(record = {}) {
const identifierType = getRecordIdentifierType(record);
const email = getRecordEmail(record);
const phoneNumber = getRecordPhoneNumber(record);
return identifierType === 'phone'
? (phoneNumber || String(record.accountIdentifier || '').trim() || email)
: (email || String(record.accountIdentifier || '').trim() || phoneNumber);
}
function getRecordSecondaryIdentifier(record = {}) {
const identifierType = getRecordIdentifierType(record);
const email = getRecordEmail(record);
const phoneNumber = getRecordPhoneNumber(record);
if (identifierType === 'phone' && email) {
return `邮箱 ${email}`;
}
if (identifierType !== 'phone' && phoneNumber) {
return `绑定手机号 ${phoneNumber}`;
}
return '';
}
function getRecordTitle(record = {}) {
const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(空账号)';
const secondaryIdentifier = getRecordSecondaryIdentifier(record);
return secondaryIdentifier
? `${primaryIdentifier} / ${secondaryIdentifier}`
: primaryIdentifier;
}
function getAccountRunRecords(currentState = state.getLatestState()) {
return (Array.isArray(currentState?.accountRunHistory) ? currentState.accountRunHistory : [])
.filter((item) => item && typeof item === 'object')
.slice()
.sort((left, right) => normalizeTimestamp(right.finishedAt) - normalizeTimestamp(left.finishedAt))
.map((record) => applyRunningDisplayState(record, currentState));
}
function summarizeAccountRunHistory(records = []) {
return records.reduce((summary, record) => {
const retryCount = normalizeRetryCount(record.retryCount);
const status = getRecordDisplayStatus(record);
summary.total += 1;
if (status === 'success') {
summary.success += 1;
} else if (status === 'running') {
summary.running += 1;
} else if (status === 'failed') {
summary.failed += 1;
} else if (status === 'stopped') {
summary.stopped += 1;
}
if (retryCount > 0) {
summary.retryRecordCount += 1;
}
summary.retryTotal += retryCount;
return summary;
}, {
total: 0,
success: 0,
running: 0,
failed: 0,
stopped: 0,
retryRecordCount: 0,
retryTotal: 0,
});
}
function formatAccountRecordTime(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '--:--';
}
const now = new Date();
const sameYear = date.getFullYear() === now.getFullYear();
const sameDay = date.toDateString() === now.toDateString();
if (sameDay) {
return date.toLocaleTimeString('zh-CN', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
timeZone: displayTimeZone,
});
}
return date.toLocaleString('zh-CN', {
hour12: false,
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
...(sameYear ? {} : { year: '2-digit' }),
timeZone: displayTimeZone,
}).replace(/\//g, '-');
}
function getStatusMeta(record = {}) {
const status = getRecordDisplayStatus(record);
if (status === 'success') {
return { kind: 'success', label: '成功' };
}
if (status === 'running') {
return { kind: 'running', label: '正在运行' };
}
if (status === 'stopped') {
return { kind: 'stopped', label: '停止' };
}
return { kind: 'failed', label: '失败' };
}
function getRecordSummaryText(record = {}) {
const status = getRecordDisplayStatus(record);
if (record.displaySummary) {
return String(record.displaySummary || '').trim();
}
if (status === 'success') {
return '流程完成';
}
if (status === 'running') {
return '正在运行';
}
return String(record.failureDetail || record.reason || '').trim()
|| String(record.failureLabel || '').trim()
|| '流程失败';
}
function getRecordTooltipText(record = {}, summaryText = '') {
const recordTitle = getRecordTitle(record);
const status = getRecordDisplayStatus(record);
const detail = String(record.displaySummary || record.failureDetail || record.reason || '').trim();
if (status === 'success' || status === 'running' || !detail || detail === recordTitle) {
return recordTitle;
}
return `${recordTitle}\n${detail}`;
}
function getFilterConfig(filterKey = activeFilter) {
return FILTER_CONFIG[filterKey] || FILTER_CONFIG.all;
}
function getFilteredRecords(records = []) {
const filterConfig = getFilterConfig(activeFilter);
return records.filter((record) => filterConfig.matches(record));
}
function pruneSelectedRecordIds(records = []) {
const availableIds = new Set(records.map((record) => buildRecordId(record)).filter(Boolean));
for (const recordId of Array.from(selectedRecordIds)) {
if (!availableIds.has(recordId)) {
selectedRecordIds.delete(recordId);
}
}
}
function setNodeHidden(node, hidden) {
if (node) {
node.hidden = Boolean(hidden);
}
}
function setNodeDisabled(node, disabled) {
if (node) {
node.disabled = Boolean(disabled);
}
}
function toggleNodeClass(node, className, enabled) {
if (!node || !className) {
return;
}
if (node.classList && typeof node.classList.toggle === 'function') {
node.classList.toggle(className, Boolean(enabled));
}
}
function setNodeText(node, value) {
if (node) {
node.textContent = String(value || '');
}
}
function setNodeAttr(node, name, value) {
if (!node || !name) {
return;
}
if (typeof node.setAttribute === 'function') {
node.setAttribute(name, String(value));
return;
}
node[name] = value;
}
function getDatasetValue(node, attrName) {
if (!node || !attrName) {
return '';
}
if (typeof node.getAttribute === 'function') {
return String(node.getAttribute(attrName) || '');
}
const dataKey = attrName
.replace(/^data-/, '')
.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
return String(node.dataset?.[dataKey] || '');
}
function findClosest(target, selector) {
if (!target || typeof target.closest !== 'function') {
return null;
}
try {
return target.closest(selector);
} catch {
return null;
}
}
function createStatChip(filterKey, value) {
const filterConfig = getFilterConfig(filterKey);
const classNames = [
'account-records-stat',
filterConfig.className,
activeFilter === filterKey ? 'is-active' : '',
].filter(Boolean).join(' ');
return `
<button
type="button"
class="${classNames}"
data-account-record-filter="${escapeHtml(filterKey)}"
aria-pressed="${activeFilter === filterKey ? 'true' : 'false'}"
>
<strong>${escapeHtml(String(value))}</strong>${escapeHtml(filterConfig.label)}
</button>
`;
}
function updateHeader(allRecords, filteredRecords) {
if (!dom.accountRecordsMeta) {
return;
}
if (!allRecords.length) {
dom.accountRecordsMeta.textContent = '暂无账号记录';
return;
}
const latestTime = formatAccountRecordTime(allRecords[0]?.finishedAt);
let metaText = `${allRecords.length} 条,最近更新于 ${latestTime}`;
if (activeFilter !== 'all') {
metaText = `${allRecords.length} 条,当前筛选 ${getFilterConfig(activeFilter).metaLabel} ${filteredRecords.length} 条,最近更新于 ${latestTime}`;
}
if (selectionMode) {
metaText += `,已选 ${selectedRecordIds.size}`;
}
dom.accountRecordsMeta.textContent = metaText;
}
function updateStats(allRecords) {
if (!dom.accountRecordsStats) {
return;
}
const summary = summarizeAccountRunHistory(allRecords);
dom.accountRecordsStats.innerHTML = [
createStatChip('all', summary.total),
createStatChip('running', summary.running),
createStatChip('success', summary.success),
createStatChip('failed', summary.failed),
createStatChip('stopped', summary.stopped),
createStatChip('retry', summary.retryTotal),
].join('');
}
function updateToolbarState(allRecords) {
const totalRecords = allRecords.length;
setNodeDisabled(dom.btnClearAccountRecords, totalRecords === 0);
setNodeDisabled(dom.btnToggleAccountRecordsSelection, totalRecords === 0);
setNodeHidden(dom.btnClearAccountRecords, selectionMode);
toggleNodeClass(dom.btnToggleAccountRecordsSelection, 'is-active', selectionMode);
setNodeAttr(dom.btnToggleAccountRecordsSelection, 'aria-pressed', selectionMode ? 'true' : 'false');
setNodeText(dom.btnToggleAccountRecordsSelection, selectionMode ? '取消多选' : '多选');
const selectedCount = selectedRecordIds.size;
setNodeHidden(dom.btnDeleteSelectedAccountRecords, !selectionMode);
setNodeDisabled(dom.btnDeleteSelectedAccountRecords, selectedCount === 0);
setNodeText(
dom.btnDeleteSelectedAccountRecords,
selectedCount > 0 ? `删除选中(${selectedCount})` : '删除选中'
);
}
function updatePagination(totalRecords) {
const totalPages = totalRecords > 0 ? Math.ceil(totalRecords / pageSize) : 0;
if (totalPages === 0) {
currentPage = 1;
} else if (currentPage > totalPages) {
currentPage = totalPages;
} else if (currentPage < 1) {
currentPage = 1;
}
setNodeText(dom.accountRecordsPageLabel, totalPages > 0 ? `${currentPage} / ${totalPages}` : '0 / 0');
setNodeDisabled(dom.btnAccountRecordsPrev, totalPages <= 1 || currentPage <= 1);
setNodeDisabled(dom.btnAccountRecordsNext, totalPages <= 1 || currentPage >= totalPages);
return totalPages;
}
function renderEmptyState(allRecords) {
if (!dom.accountRecordsList) {
return;
}
const message = allRecords.length
? `当前筛选“${getFilterConfig(activeFilter).metaLabel}”下暂无记录`
: '暂无账号记录';
dom.accountRecordsList.innerHTML = `<div class="account-records-empty">${escapeHtml(message)}</div>`;
}
function renderRecordList(allRecords, filteredRecords) {
if (!dom.accountRecordsList) {
return;
}
const totalPages = updatePagination(filteredRecords.length);
if (!filteredRecords.length) {
renderEmptyState(allRecords);
return;
}
const startIndex = (currentPage - 1) * pageSize;
const visibleRecords = filteredRecords.slice(startIndex, startIndex + pageSize);
dom.accountRecordsList.innerHTML = visibleRecords.map((record) => {
const recordId = buildRecordId(record);
const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(空账号)';
const secondaryIdentifier = getRecordSecondaryIdentifier(record);
const statusMeta = getStatusMeta(record);
const summaryText = getRecordSummaryText(record);
const recordTitle = getRecordTooltipText(record, summaryText);
const retryCount = normalizeRetryCount(record.retryCount);
const isSelected = selectedRecordIds.has(recordId);
const itemClassNames = [
'account-record-item',
`is-${statusMeta.kind}`,
selectionMode ? 'is-selectable' : '',
isSelected ? 'is-selected' : '',
].filter(Boolean).join(' ');
const selectionMarkup = selectionMode
? `
<label class="account-record-item-check" data-account-record-toggle="${escapeHtml(recordId)}">
<input
type="checkbox"
data-account-record-checkbox="${escapeHtml(recordId)}"
${isSelected ? 'checked' : ''}
/>
</label>
`
: '';
return `
<div
class="${itemClassNames}"
data-account-record-id="${escapeHtml(recordId)}"
title="${escapeHtml(recordTitle)}"
>
<div class="account-record-item-top">
<div class="account-record-item-email-row">
${selectionMarkup}
<div class="account-record-item-identity">
<div class="account-record-item-email mono">${escapeHtml(primaryIdentifier)}</div>
${secondaryIdentifier ? `<div class="account-record-item-secondary mono">${escapeHtml(secondaryIdentifier)}</div>` : ''}
</div>
</div>
<div class="account-record-item-side">
<span class="account-record-item-status">${escapeHtml(statusMeta.label)}</span>
<span class="account-record-item-time mono">${escapeHtml(formatAccountRecordTime(record.finishedAt))}</span>
</div>
</div>
<div class="account-record-item-bottom">
<div class="account-record-item-summary">${escapeHtml(summaryText)}</div>
<span class="account-record-item-retry mono">重试 ${escapeHtml(String(retryCount))}</span>
</div>
</div>
`;
}).join('');
if (totalPages <= 1) {
setNodeText(dom.accountRecordsPageLabel, '1 / 1');
}
}
function render(currentState = state.getLatestState()) {
const allRecords = getAccountRunRecords(currentState);
pruneSelectedRecordIds(allRecords);
if (!allRecords.length) {
selectionMode = false;
}
const filteredRecords = getFilteredRecords(allRecords);
updateHeader(allRecords, filteredRecords);
updateStats(allRecords);
updateToolbarState(allRecords);
renderRecordList(allRecords, filteredRecords);
}
function openPanel() {
setNodeHidden(dom.accountRecordsOverlay, false);
render();
}
function closePanel() {
setNodeHidden(dom.accountRecordsOverlay, true);
}
function resetSelection() {
selectedRecordIds.clear();
}
function setSelectionMode(nextValue) {
const nextSelectionMode = Boolean(nextValue);
if (!nextSelectionMode) {
resetSelection();
}
selectionMode = nextSelectionMode;
currentPage = 1;
render();
}
function toggleSelectionMode() {
setSelectionMode(!selectionMode);
}
function toggleRecordSelection(recordId, forceSelected = null) {
const normalizedRecordId = String(recordId || '').trim().toLowerCase();
if (!selectionMode || !normalizedRecordId) {
return;
}
const shouldSelect = forceSelected === null
? !selectedRecordIds.has(normalizedRecordId)
: Boolean(forceSelected);
if (shouldSelect) {
selectedRecordIds.add(normalizedRecordId);
} else {
selectedRecordIds.delete(normalizedRecordId);
}
}
async function clearRecords() {
const records = getAccountRunRecords();
if (!records.length) {
helpers.showToast?.('没有可清理的账号记录。', 'warn', 1800);
return;
}
const confirmed = await helpers.openConfirmModal({
title: '清理账号记录',
message: '确认清理当前全部账号记录吗?该操作会同时清空面板记录与本地同步快照。',
confirmLabel: '确认清理',
confirmVariant: 'btn-danger',
});
if (!confirmed) {
return;
}
const response = await runtime.sendMessage({
type: 'CLEAR_ACCOUNT_RUN_HISTORY',
source: 'sidepanel',
});
if (response?.error) {
throw new Error(response.error);
}
activeFilter = 'all';
currentPage = 1;
selectionMode = false;
resetSelection();
state.syncLatestState({ accountRunHistory: [] });
helpers.showToast?.(`已清理 ${Math.max(0, Number(response?.clearedCount) || 0)} 条账号记录。`, 'success', 2200);
}
async function deleteSelectedRecords() {
const recordIds = Array.from(selectedRecordIds).filter(Boolean);
if (!recordIds.length) {
helpers.showToast?.('请先勾选要删除的账号记录。', 'warn', 1800);
return;
}
const confirmed = await helpers.openConfirmModal({
title: '删除选中记录',
message: `确认删除选中的 ${recordIds.length} 条账号记录吗?该操作会同步更新本地 helper 快照。`,
confirmLabel: '确认删除',
confirmVariant: 'btn-danger',
});
if (!confirmed) {
return;
}
const response = await runtime.sendMessage({
type: 'DELETE_ACCOUNT_RUN_HISTORY_RECORDS',
source: 'sidepanel',
payload: {
recordIds,
},
});
if (response?.error) {
throw new Error(response.error);
}
const existingRecords = getAccountRunRecords();
const selectedIds = new Set(recordIds);
const nextRecords = existingRecords.filter((record) => !selectedIds.has(buildRecordId(record)));
resetSelection();
state.syncLatestState({ accountRunHistory: nextRecords });
helpers.showToast?.(`已删除 ${Math.max(0, Number(response?.deletedCount) || 0)} 条账号记录。`, 'success', 2200);
}
function handleStatsClick(event) {
const filterNode = findClosest(event?.target, '[data-account-record-filter]');
if (!filterNode) {
return;
}
const nextFilter = getDatasetValue(filterNode, 'data-account-record-filter');
if (!FILTER_CONFIG[nextFilter]) {
return;
}
activeFilter = activeFilter === nextFilter && nextFilter !== 'all'
? 'all'
: nextFilter;
currentPage = 1;
render();
}
function handleRecordListClick(event) {
if (!selectionMode) {
return;
}
const toggleNode = findClosest(event?.target, '[data-account-record-toggle]');
if (toggleNode) {
const recordId = getDatasetValue(toggleNode, 'data-account-record-toggle');
const explicitChecked = typeof event?.target?.checked === 'boolean' ? event.target.checked : null;
toggleRecordSelection(recordId, explicitChecked);
render();
return;
}
const recordNode = findClosest(event?.target, '[data-account-record-id]');
if (!recordNode) {
return;
}
toggleRecordSelection(getDatasetValue(recordNode, 'data-account-record-id'));
render();
}
function bindEvents() {
if (eventsBound) {
return;
}
eventsBound = true;
dom.btnOpenAccountRecords?.addEventListener('click', () => {
openPanel();
});
dom.btnCloseAccountRecords?.addEventListener('click', () => {
closePanel();
});
dom.accountRecordsOverlay?.addEventListener('click', (event) => {
if (event.target === dom.accountRecordsOverlay) {
closePanel();
}
});
dom.accountRecordsStats?.addEventListener('click', (event) => {
handleStatsClick(event);
});
dom.accountRecordsList?.addEventListener('click', (event) => {
handleRecordListClick(event);
});
dom.btnAccountRecordsPrev?.addEventListener('click', () => {
if (currentPage <= 1) {
return;
}
currentPage -= 1;
render();
});
dom.btnAccountRecordsNext?.addEventListener('click', () => {
currentPage += 1;
render();
});
dom.btnToggleAccountRecordsSelection?.addEventListener('click', () => {
toggleSelectionMode();
});
dom.btnDeleteSelectedAccountRecords?.addEventListener('click', async () => {
try {
await deleteSelectedRecords();
} catch (error) {
helpers.showToast?.(`删除账号记录失败:${error.message}`, 'error');
}
});
dom.btnClearAccountRecords?.addEventListener('click', async () => {
try {
await clearRecords();
} catch (error) {
helpers.showToast?.(`清理账号记录失败:${error.message}`, 'error');
}
});
}
function reset() {
currentPage = 1;
activeFilter = 'all';
selectionMode = false;
resetSelection();
closePanel();
render();
}
return {
bindEvents,
clearRecords,
closePanel,
deleteSelectedRecords,
openPanel,
render,
reset,
setSelectionMode,
summarizeAccountRunHistory,
toggleSelectionMode,
};
}
globalScope.SidepanelAccountRecordsManager = {
createAccountRecordsManager,
};
})(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,116 @@
(() => {
const PORTAL_BASE_URL = '';
const CONTENT_SUMMARY_API_URL = '';
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(),
text: String(item?.text || '').trim(),
isEnabled: Boolean(item?.is_enabled ?? item?.isEnabled),
hasContent: Boolean(item?.has_content ?? item?.hasContent),
isVisible: Boolean(item?.is_visible ?? item?.isVisible),
updatedAt: String(item?.updated_at ?? item?.updatedAt ?? '').trim(),
updatedAtDisplay: String(item?.updated_at_display ?? item?.updatedAtDisplay ?? '').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() {
return buildSnapshot({});
}
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,
};
})();
+503
View File
@@ -0,0 +1,503 @@
(function attachSidepanelContributionMode(globalScope) {
const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']);
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'expired', 'error']);
const DEFAULT_COPY = '当前账号将用于支持项目维护。扩展会自动申请贡献登录地址并持续跟踪授权状态;如检测到回调地址,会自动提交,并继续等待服务端确认。';
const CONTRIBUTION_SOURCE_CPA = 'cpa';
const CONTRIBUTION_SOURCE_SUB2API = 'sub2api';
const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池';
function createContributionModeManager(context = {}) {
const {
state,
dom,
helpers,
runtime,
constants = {},
} = context;
const contributionPortalUrl = constants.contributionPortalUrl || '';
const guideRepositoryUrl = constants.guideRepositoryUrl || 'https://github.com/FoundZiGu/GuJumpgate';
const contributionUploadUrl = constants.contributionUploadUrl || '';
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 normalizeContributionSource(value = '') {
const normalized = normalizeString(value).toLowerCase();
return normalized === CONTRIBUTION_SOURCE_SUB2API
? CONTRIBUTION_SOURCE_SUB2API
: CONTRIBUTION_SOURCE_CPA;
}
function getContributionSource(currentState = getLatestState()) {
return normalizeContributionSource(currentState.contributionSource || currentState.panelMode);
}
function getContributionSourceLabel(currentState = getLatestState()) {
return getContributionSource(currentState) === CONTRIBUTION_SOURCE_SUB2API ? 'SUB2API' : 'CPA';
}
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 = false;
dom.btnContributionMode.title = '打开项目仓库说明页';
}
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') {
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()) {
const statusMessage = normalizeString(currentState.contributionStatusMessage);
if (statusMessage) {
return statusMessage;
}
if (getContributionSource(currentState) === CONTRIBUTION_SOURCE_SUB2API) {
const groupName = normalizeString(currentState.contributionTargetGroupName) || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME;
return `当前账号将用于支持项目维护。贡献会通过 SUB2API 完成,并固定写入 ${groupName} 分组;如检测到回调地址,扩展会自动提交并等待服务端确认。`;
}
return DEFAULT_COPY;
}
function getContributionPortalPageUrl() {
return normalizeString(contributionPortalUrl);
}
function getContributionUploadPageUrl() {
return normalizeString(contributionUploadUrl);
}
function openContributionPortalPage() {
const targetUrl = getContributionPortalPageUrl();
if (!targetUrl) {
return;
}
helpers.openExternalUrl?.(targetUrl);
}
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;
const sourceLabel = getContributionSourceLabel(currentState);
if (enabled && dom.selectPanelMode) {
dom.selectPanelMode.value = getContributionSource(currentState);
}
helpers.updatePanelModeUI?.();
helpers.updateAccountRunHistorySettingsUI?.();
if (dom.contributionModePanel) {
dom.contributionModePanel.hidden = !enabled;
}
if (dom.contributionModeText) {
dom.contributionModeText.textContent = getSummaryText({
contributionSource: currentState.contributionSource,
contributionTargetGroupName: currentState.contributionTargetGroupName,
});
}
if (dom.contributionModeBadge) {
dom.contributionModeBadge.textContent = enabled ? sourceLabel : '';
}
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 = !getContributionUploadPageUrl();
}
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 () => {
try {
helpers.openExternalUrl?.(guideRepositoryUrl);
} catch (error) {
helpers.showToast?.(`打开说明页失败:${error.message}`, 'error');
}
});
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);
+534
View File
@@ -0,0 +1,534 @@
(function attachSidepanelCustomEmailPoolManager(globalScope) {
function createCustomEmailPoolManager(context = {}) {
const {
dom,
helpers,
state,
actions,
constants = {},
} = context;
const copyIcon = constants.copyIcon || '';
let renderedEntries = [];
let selectedEntryIds = new Set();
let searchTerm = '';
let filterMode = 'all';
let refreshQueued = false;
let loading = false;
function normalizeEmail(value = '') {
return String(value || '').trim().toLowerCase();
}
function isValidEmail(value = '') {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizeEmail(value));
}
function createEntryId() {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return `custom-pool-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}
function normalizeEntry(rawEntry = {}) {
const email = normalizeEmail(rawEntry?.email || '');
if (!isValidEmail(email)) {
return null;
}
return {
id: String(rawEntry?.id || createEntryId()),
email,
enabled: rawEntry?.enabled !== undefined ? Boolean(rawEntry.enabled) : true,
used: Boolean(rawEntry?.used),
note: String(rawEntry?.note || '').trim(),
lastUsedAt: Number.isFinite(Number(rawEntry?.lastUsedAt)) ? Number(rawEntry.lastUsedAt) : 0,
};
}
function normalizeEntries(entries = []) {
if (!Array.isArray(entries)) {
return [];
}
const seenEmails = new Set();
const normalized = [];
for (const entry of entries) {
const item = normalizeEntry(entry);
if (!item) continue;
if (seenEmails.has(item.email)) continue;
seenEmails.add(item.email);
normalized.push(item);
}
return normalized;
}
function withCurrentFlag(entries = renderedEntries) {
const currentEmail = normalizeEmail(state.getCurrentEmail?.());
return normalizeEntries(entries).map((entry) => ({
...entry,
current: Boolean(currentEmail) && entry.email === currentEmail,
}));
}
function normalizeSearchText(value = '') {
return String(value || '').trim().toLowerCase();
}
function getFilteredEntries(entries = renderedEntries) {
const normalizedSearchTerm = normalizeSearchText(searchTerm);
return withCurrentFlag(entries).filter((entry) => {
const matchesFilter = (() => {
switch (filterMode) {
case 'enabled': return Boolean(entry.enabled);
case 'disabled': return !entry.enabled;
case 'used': return Boolean(entry.used);
case 'unused': return !entry.used;
case 'current': return Boolean(entry.current);
default: return true;
}
})();
if (!matchesFilter) return false;
if (!normalizedSearchTerm) return true;
const haystack = [
entry.email,
entry.note,
entry.enabled ? 'enabled 启用' : 'disabled 停用',
entry.used ? 'used 已用' : 'unused 未用',
entry.current ? 'current 当前' : '',
].join(' ').toLowerCase();
return haystack.includes(normalizedSearchTerm);
});
}
function pruneSelection(entries = renderedEntries) {
const existingIds = new Set(withCurrentFlag(entries).map((entry) => String(entry.id)));
selectedEntryIds = new Set([...selectedEntryIds].filter((id) => existingIds.has(id)));
}
function updateBulkUi(visibleEntries = getFilteredEntries()) {
if (!dom.checkboxCustomEmailPoolSelectAll || !dom.customEmailPoolSelectionSummary) {
return;
}
const visibleIds = visibleEntries.map((entry) => String(entry.id));
const selectedVisibleCount = visibleIds.filter((id) => selectedEntryIds.has(id)).length;
const hasVisible = visibleIds.length > 0;
const hasSelection = selectedEntryIds.size > 0;
dom.checkboxCustomEmailPoolSelectAll.checked = hasVisible && selectedVisibleCount === visibleIds.length;
dom.checkboxCustomEmailPoolSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleIds.length;
dom.checkboxCustomEmailPoolSelectAll.disabled = loading || !hasVisible;
dom.customEmailPoolSelectionSummary.textContent = `已选 ${selectedEntryIds.size} 个(当前显示 ${visibleIds.length} 个)`;
if (dom.btnCustomEmailPoolBulkUsed) dom.btnCustomEmailPoolBulkUsed.disabled = loading || !hasSelection;
if (dom.btnCustomEmailPoolBulkUnused) dom.btnCustomEmailPoolBulkUnused.disabled = loading || !hasSelection;
if (dom.btnCustomEmailPoolBulkEnable) dom.btnCustomEmailPoolBulkEnable.disabled = loading || !hasSelection;
if (dom.btnCustomEmailPoolBulkDisable) dom.btnCustomEmailPoolBulkDisable.disabled = loading || !hasSelection;
if (dom.btnCustomEmailPoolBulkDelete) dom.btnCustomEmailPoolBulkDelete.disabled = loading || !hasSelection;
}
function setLoadingState(nextLoading, summary = '') {
loading = Boolean(nextLoading);
if (dom.btnCustomEmailPoolImport) dom.btnCustomEmailPoolImport.disabled = loading;
if (dom.btnCustomEmailPoolRefresh) dom.btnCustomEmailPoolRefresh.disabled = loading;
if (dom.btnCustomEmailPoolClearUsed) dom.btnCustomEmailPoolClearUsed.disabled = loading;
if (dom.btnCustomEmailPoolDeleteAll) dom.btnCustomEmailPoolDeleteAll.disabled = loading;
if (dom.inputCustomEmailPoolImport) dom.inputCustomEmailPoolImport.disabled = loading;
if (summary && dom.customEmailPoolSummary) {
dom.customEmailPoolSummary.textContent = summary;
}
updateBulkUi(getFilteredEntries());
}
function renderCustomEmailPoolEntries(entries = state.getEntries?.()) {
if (!dom.customEmailPoolList || !dom.customEmailPoolSummary) {
return;
}
renderedEntries = normalizeEntries(entries);
pruneSelection(renderedEntries);
dom.customEmailPoolList.innerHTML = '';
if (!renderedEntries.length) {
selectedEntryIds.clear();
dom.customEmailPoolList.innerHTML = '<div class="luckmail-empty">还没有自定义邮箱,先导入一批邮箱再开始。</div>';
dom.customEmailPoolSummary.textContent = '导入你提前准备好的注册邮箱,每行一个邮箱地址。';
if (dom.btnCustomEmailPoolClearUsed) dom.btnCustomEmailPoolClearUsed.disabled = true;
if (dom.btnCustomEmailPoolDeleteAll) dom.btnCustomEmailPoolDeleteAll.disabled = true;
updateBulkUi([]);
return;
}
const entriesWithCurrent = withCurrentFlag(renderedEntries);
const usedCount = entriesWithCurrent.filter((entry) => entry.used).length;
const enabledCount = entriesWithCurrent.filter((entry) => entry.enabled).length;
dom.customEmailPoolSummary.textContent = `已加载 ${entriesWithCurrent.length} 个邮箱,其中 ${enabledCount} 个启用,${usedCount} 个已标记为已用。`;
if (dom.btnCustomEmailPoolClearUsed) dom.btnCustomEmailPoolClearUsed.disabled = loading || usedCount === 0;
if (dom.btnCustomEmailPoolDeleteAll) dom.btnCustomEmailPoolDeleteAll.disabled = loading || entriesWithCurrent.length === 0;
const visibleEntries = getFilteredEntries(entriesWithCurrent);
if (!visibleEntries.length) {
dom.customEmailPoolList.innerHTML = '<div class="luckmail-empty">没有匹配当前筛选条件的邮箱。</div>';
updateBulkUi([]);
return;
}
for (const entry of visibleEntries) {
const entryId = String(entry.id);
const item = document.createElement('div');
item.className = `luckmail-item${entry.current ? ' is-current' : ''}`;
item.innerHTML = `
<input class="luckmail-item-check" type="checkbox" data-action="select" ${selectedEntryIds.has(entryId) ? 'checked' : ''} />
<div class="luckmail-item-main">
<div class="luckmail-item-email-row">
<div class="luckmail-item-email">${helpers.escapeHtml(entry.email || '(未知邮箱)')}</div>
<button
class="hotmail-copy-btn"
type="button"
data-action="copy-email"
title="复制邮箱"
aria-label="复制邮箱 ${helpers.escapeHtml(entry.email || '')}"
>${copyIcon}</button>
</div>
<div class="luckmail-item-meta">
${entry.current ? '<span class="luckmail-tag current">当前</span>' : ''}
${entry.used ? '<span class="luckmail-tag used">已用</span>' : '<span class="luckmail-tag active">未用</span>'}
${entry.enabled ? '<span class="luckmail-tag active">启用</span>' : '<span class="luckmail-tag disabled">停用</span>'}
${entry.note ? `<span class="luckmail-tag">${helpers.escapeHtml(entry.note)}</span>` : ''}
</div>
</div>
<div class="luckmail-item-actions">
<button class="btn btn-outline btn-xs" type="button" data-action="use">使用此邮箱</button>
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-used">${helpers.escapeHtml(entry.used ? '标记未用' : '标记已用')}</button>
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-enabled">${helpers.escapeHtml(entry.enabled ? '停用' : '启用')}</button>
<button class="btn btn-outline btn-xs" type="button" data-action="delete">删除</button>
</div>
`;
item.querySelector('[data-action="select"]').addEventListener('change', (event) => {
if (event.target.checked) {
selectedEntryIds.add(entryId);
} else {
selectedEntryIds.delete(entryId);
}
updateBulkUi(visibleEntries);
});
item.querySelector('[data-action="copy-email"]').addEventListener('click', async () => {
await helpers.copyTextToClipboard(entry.email || '');
helpers.showToast('邮箱已复制', 'success', 1600);
});
item.querySelector('[data-action="use"]').addEventListener('click', async () => {
try {
setLoadingState(true, '正在切换当前邮箱...');
await actions.setRuntimeEmail?.(entry.email);
helpers.showToast(`已切换到 ${entry.email}`, 'success', 1800);
queueCustomEmailPoolRefresh();
} catch (error) {
helpers.showToast(`切换邮箱失败:${error.message}`, 'error');
} finally {
setLoadingState(false);
}
});
item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => {
await patchEntries((entriesList) => entriesList.map((candidate) => (
String(candidate.id) === entryId
? {
...candidate,
used: !entry.used,
lastUsedAt: !entry.used ? Date.now() : candidate.lastUsedAt,
}
: candidate
)));
});
item.querySelector('[data-action="toggle-enabled"]').addEventListener('click', async () => {
await patchEntries((entriesList) => entriesList.map((candidate) => (
String(candidate.id) === entryId
? { ...candidate, enabled: !entry.enabled }
: candidate
)));
});
item.querySelector('[data-action="delete"]').addEventListener('click', async () => {
await deleteEntries({
ids: [entry.id],
}, `确认删除 ${entry.email} 吗?此操作不可撤销。`);
});
dom.customEmailPoolList.appendChild(item);
}
updateBulkUi(visibleEntries);
}
async function patchEntries(mutator) {
const previousEntries = normalizeEntries(state.getEntries?.() || []);
const nextEntries = normalizeEntries(mutator(previousEntries.map((entry) => ({ ...entry }))));
setLoadingState(true, '正在更新自定义邮箱池...');
state.setEntries?.(nextEntries);
renderCustomEmailPoolEntries(nextEntries);
try {
await actions.persistEntries?.();
} catch (error) {
state.setEntries?.(previousEntries);
renderCustomEmailPoolEntries(previousEntries);
helpers.showToast(`更新自定义邮箱池失败:${error.message}`, 'error');
} finally {
setLoadingState(false);
}
}
async function deleteEntries(payload = {}, confirmMessage = '') {
const confirmed = await helpers.openConfirmModal({
title: '删除邮箱',
message: confirmMessage || '确认删除选中的邮箱吗?此操作不可撤销。',
confirmLabel: '确认删除',
confirmVariant: 'btn-danger',
});
if (!confirmed) {
return;
}
const ids = Array.isArray(payload.ids)
? payload.ids.map((id) => String(id || '').trim()).filter(Boolean)
: [];
const mode = String(payload.mode || '').trim().toLowerCase();
await patchEntries((entriesList) => {
if (mode === 'all') {
selectedEntryIds.clear();
return [];
}
if (mode === 'used') {
const usedIds = new Set(entriesList.filter((entry) => entry.used).map((entry) => String(entry.id)));
usedIds.forEach((id) => selectedEntryIds.delete(id));
return entriesList.filter((entry) => !entry.used);
}
const targetIds = new Set(ids);
ids.forEach((id) => selectedEntryIds.delete(id));
return entriesList.filter((entry) => !targetIds.has(String(entry.id)));
});
}
async function importEntriesFromTextarea() {
const text = String(dom.inputCustomEmailPoolImport?.value || '');
if (!text.trim()) {
helpers.showToast('请先粘贴邮箱列表,每行一个邮箱。', 'warn');
return;
}
const previousEntries = normalizeEntries(state.getEntries?.() || []);
const knownEmails = new Set(previousEntries.map((entry) => entry.email));
const importedEntries = [];
let skippedCount = 0;
for (const line of String(text || '').split(/[\r\n,;]+/)) {
const email = normalizeEmail(line);
if (!email) {
continue;
}
if (!isValidEmail(email) || knownEmails.has(email)) {
skippedCount += 1;
continue;
}
knownEmails.add(email);
importedEntries.push({
id: createEntryId(),
email,
enabled: true,
used: false,
note: '',
lastUsedAt: 0,
});
}
if (!importedEntries.length && skippedCount > 0) {
helpers.showToast('没有可导入的新邮箱(可能都重复或无效)。', 'warn');
return;
}
const nextEntries = normalizeEntries([...previousEntries, ...importedEntries]);
setLoadingState(true, '正在导入邮箱...');
state.setEntries?.(nextEntries);
renderCustomEmailPoolEntries(nextEntries);
try {
await actions.persistEntries?.();
if (dom.inputCustomEmailPoolImport) {
dom.inputCustomEmailPoolImport.value = '';
}
helpers.showToast(
skippedCount > 0
? `已导入 ${importedEntries.length} 个邮箱,跳过 ${skippedCount} 条无效或重复数据。`
: `已导入 ${importedEntries.length} 个邮箱。`,
importedEntries.length > 0 ? 'success' : 'warn',
2400
);
} catch (error) {
state.setEntries?.(previousEntries);
renderCustomEmailPoolEntries(previousEntries);
helpers.showToast(`导入邮箱失败:${error.message}`, 'error');
} finally {
setLoadingState(false);
}
}
async function refreshCustomEmailPoolEntries(options = {}) {
const { silent = false } = options;
if (state.isVisible && !state.isVisible()) {
return;
}
if (!silent) {
setLoadingState(true, '正在刷新自定义邮箱池...');
}
renderCustomEmailPoolEntries(state.getEntries?.());
if (!silent) {
setLoadingState(false);
}
}
function queueCustomEmailPoolRefresh() {
if (refreshQueued) return;
refreshQueued = true;
setTimeout(() => {
refreshQueued = false;
refreshCustomEmailPoolEntries({ silent: true });
}, 120);
}
function reset() {
selectedEntryIds.clear();
searchTerm = '';
filterMode = 'all';
if (dom.inputCustomEmailPoolSearch) dom.inputCustomEmailPoolSearch.value = '';
if (dom.selectCustomEmailPoolFilter) dom.selectCustomEmailPoolFilter.value = 'all';
if (dom.customEmailPoolList) {
dom.customEmailPoolList.innerHTML = '';
}
if (dom.customEmailPoolSummary) {
dom.customEmailPoolSummary.textContent = '导入你提前准备好的注册邮箱,每行一个邮箱地址。';
}
updateBulkUi([]);
}
function bindEvents() {
dom.btnCustomEmailPoolRefresh?.addEventListener('click', async () => {
await refreshCustomEmailPoolEntries();
});
dom.btnCustomEmailPoolImport?.addEventListener('click', async () => {
await importEntriesFromTextarea();
});
dom.inputCustomEmailPoolImport?.addEventListener('keydown', async (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
event.preventDefault();
await importEntriesFromTextarea();
}
});
dom.inputCustomEmailPoolSearch?.addEventListener('input', (event) => {
searchTerm = normalizeSearchText(event.target.value);
renderCustomEmailPoolEntries(renderedEntries);
});
dom.selectCustomEmailPoolFilter?.addEventListener('change', (event) => {
filterMode = String(event.target.value || 'all');
renderCustomEmailPoolEntries(renderedEntries);
});
dom.checkboxCustomEmailPoolSelectAll?.addEventListener('change', (event) => {
const visibleEntries = getFilteredEntries(renderedEntries);
const visibleIds = visibleEntries.map((entry) => String(entry.id));
if (event.target.checked) {
visibleIds.forEach((id) => selectedEntryIds.add(id));
} else {
visibleIds.forEach((id) => selectedEntryIds.delete(id));
}
renderCustomEmailPoolEntries(renderedEntries);
});
dom.btnCustomEmailPoolBulkUsed?.addEventListener('click', async () => {
const targetIds = new Set([...selectedEntryIds]);
await patchEntries((entriesList) => entriesList.map((entry) => (
targetIds.has(String(entry.id))
? { ...entry, used: true, lastUsedAt: entry.lastUsedAt || Date.now() }
: entry
)));
});
dom.btnCustomEmailPoolBulkUnused?.addEventListener('click', async () => {
const targetIds = new Set([...selectedEntryIds]);
await patchEntries((entriesList) => entriesList.map((entry) => (
targetIds.has(String(entry.id))
? { ...entry, used: false }
: entry
)));
});
dom.btnCustomEmailPoolBulkEnable?.addEventListener('click', async () => {
const targetIds = new Set([...selectedEntryIds]);
await patchEntries((entriesList) => entriesList.map((entry) => (
targetIds.has(String(entry.id))
? { ...entry, enabled: true }
: entry
)));
});
dom.btnCustomEmailPoolBulkDisable?.addEventListener('click', async () => {
const targetIds = new Set([...selectedEntryIds]);
await patchEntries((entriesList) => entriesList.map((entry) => (
targetIds.has(String(entry.id))
? { ...entry, enabled: false }
: entry
)));
});
dom.btnCustomEmailPoolBulkDelete?.addEventListener('click', async () => {
await deleteEntries({
ids: [...selectedEntryIds],
}, `确认删除当前选中的 ${selectedEntryIds.size} 个邮箱吗?此操作不可撤销。`);
});
dom.btnCustomEmailPoolClearUsed?.addEventListener('click', async () => {
await deleteEntries({
mode: 'used',
}, '确认删除当前所有已用邮箱吗?此操作不可撤销。');
});
dom.btnCustomEmailPoolDeleteAll?.addEventListener('click', async () => {
await deleteEntries({
mode: 'all',
}, '确认删除当前全部邮箱吗?此操作不可撤销。');
});
}
return {
bindEvents,
queueCustomEmailPoolRefresh,
refreshCustomEmailPoolEntries,
renderCustomEmailPoolEntries,
reset,
};
}
globalScope.SidepanelCustomEmailPoolManager = {
createCustomEmailPoolManager,
};
})(typeof window !== 'undefined' ? window : globalThis);
+228
View File
@@ -0,0 +1,228 @@
(function attachSidepanelEditableListPicker(globalScope) {
const editableListPickers = [];
function splitEditableListValues(value = '') {
return String(value || '')
.split(/[\r\n,,、]+/)
.map((name) => name.trim())
.filter(Boolean);
}
function normalizeEditableListValues(...sources) {
const values = [];
const seen = new Set();
const append = (value) => {
if (Array.isArray(value)) {
value.forEach(append);
return;
}
for (const item of splitEditableListValues(value)) {
const key = item.toLowerCase();
if (!key || seen.has(key)) {
continue;
}
seen.add(key);
values.push(item);
}
};
sources.forEach(append);
return values;
}
function createEditableListPicker(config = {}) {
const {
root,
input,
trigger,
current,
menu,
emptyLabel = '请先添加',
fallbackItems = [],
minItems = 0,
deleteLabel = '删除',
itemLabel = '项目',
normalizeItems = normalizeEditableListValues,
normalizeValue = (value) => String(value || '').trim(),
getItemValue = (item) => String(item || '').trim(),
getItemLabel = (item) => getItemValue(item),
getItemDeleteLabel = (item) => getItemLabel(item),
onDelete = null,
onDeleteError = null,
} = config;
const picker = {
root,
input,
trigger,
current,
menu,
items: [],
open: false,
};
const getFallbackItems = () => normalizeItems(fallbackItems);
const getNormalizedItemValue = (item) => normalizeValue(getItemValue(item));
const findItemByValue = (value) => {
const normalized = normalizeValue(value);
return picker.items.find((item) => getNormalizedItemValue(item) === normalized) || null;
};
const reportDeleteError = (error) => {
const fallbackMessage = `${deleteLabel}${itemLabel}失败。`;
if (typeof onDeleteError === 'function') {
onDeleteError(error, fallbackMessage);
return;
}
if (typeof globalScope.showToast === 'function') {
globalScope.showToast(error?.message || fallbackMessage, 'error');
}
};
picker.setOpen = (open) => {
picker.open = Boolean(open) && !trigger?.disabled;
if (menu) {
menu.hidden = !picker.open;
}
if (trigger) {
trigger.setAttribute('aria-expanded', picker.open ? 'true' : 'false');
trigger.classList?.toggle('is-open', picker.open);
}
};
picker.close = () => {
picker.setOpen(false);
};
picker.setVisible = (visible) => {
if (root) {
root.style.display = visible ? '' : 'none';
}
if (!visible) {
picker.close();
}
};
picker.setSelection = (value, options = {}) => {
const fallback = getNormalizedItemValue(picker.items[0]) || getNormalizedItemValue(getFallbackItems()[0]) || '';
const selected = normalizeValue(value) || fallback;
const selectedItem = findItemByValue(selected);
if (input) {
input.value = selected;
if (options.emit && typeof input.dispatchEvent === 'function') {
input.dispatchEvent(new Event('change', { bubbles: true }));
}
}
if (current) {
current.textContent = selectedItem ? getItemLabel(selectedItem) : (selected || emptyLabel);
}
};
picker.render = (items = [], selectedValue = '') => {
const normalizedItems = normalizeItems(items);
picker.items = normalizedItems.length ? normalizedItems : getFallbackItems();
const inputValue = normalizeValue(input?.value);
const selected = normalizeValue(selectedValue)
|| (findItemByValue(inputValue) ? inputValue : '')
|| getNormalizedItemValue(picker.items[0])
|| '';
if (trigger) {
trigger.disabled = picker.items.length === 0;
}
if (picker.items.length === 0) {
if (menu) {
menu.innerHTML = '';
}
picker.setSelection('', { emit: false });
picker.close();
return;
}
if (
!menu
|| typeof menu.appendChild !== 'function'
|| typeof globalScope.document === 'undefined'
|| typeof globalScope.document.createElement !== 'function'
) {
picker.setSelection(selected, { emit: false });
return;
}
menu.innerHTML = '';
picker.items.forEach((item) => {
const itemValue = getNormalizedItemValue(item);
const row = globalScope.document.createElement('div');
row.className = 'editable-list-option-row';
const option = globalScope.document.createElement('button');
option.type = 'button';
option.className = 'editable-list-option';
option.setAttribute('role', 'option');
option.setAttribute('aria-selected', itemValue === selected ? 'true' : 'false');
option.textContent = getItemLabel(item);
option.addEventListener('click', () => {
picker.setSelection(itemValue, { emit: true });
picker.close();
});
const deleteButton = globalScope.document.createElement('button');
deleteButton.type = 'button';
deleteButton.className = 'editable-list-delete';
deleteButton.textContent = deleteLabel;
deleteButton.title = `${deleteLabel}${itemLabel} ${getItemDeleteLabel(item)}`;
deleteButton.setAttribute('aria-label', `${deleteLabel}${itemLabel} ${getItemDeleteLabel(item)}`);
deleteButton.disabled = picker.items.length <= minItems;
deleteButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (typeof onDelete === 'function') {
Promise.resolve(onDelete(itemValue, item)).catch(reportDeleteError);
}
});
row.appendChild(option);
row.appendChild(deleteButton);
menu.appendChild(row);
});
picker.setSelection(selected, { emit: false });
};
trigger?.addEventListener('click', (event) => {
event.stopPropagation();
editableListPickers.forEach((item) => {
if (item !== picker) {
item.close();
}
});
picker.setOpen(!picker.open);
});
trigger?.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
picker.close();
}
});
menu?.addEventListener('click', (event) => {
event.stopPropagation();
});
editableListPickers.push(picker);
return picker;
}
function closeEditableListPickers() {
editableListPickers.forEach((picker) => picker.close());
}
function isClickInsideEditableListPicker(target) {
return editableListPickers.some((picker) => Boolean(picker.root?.contains(target)));
}
globalScope.SidepanelEditableListPicker = {
closeEditableListPickers,
createEditableListPicker,
isClickInsideEditableListPicker,
normalizeEditableListValues,
splitEditableListValues,
};
})(window);
+269
View File
@@ -0,0 +1,269 @@
(function attachSidepanelFormDialog(globalScope) {
const FORM_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 FORM_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>';
function syncPasswordToggleButton(button, input, labels) {
if (!button || !input) return;
const isHidden = input.type === 'password';
button.innerHTML = isHidden ? FORM_EYE_OPEN_ICON : FORM_EYE_CLOSED_ICON;
button.setAttribute('aria-label', isHidden ? labels.show : labels.hide);
button.title = isHidden ? labels.show : labels.hide;
}
function createFormDialog(context = {}) {
const {
overlay = null,
titleNode = null,
closeButton = null,
messageNode = null,
alertNode = null,
fieldsContainer = null,
cancelButton = null,
confirmButton = null,
documentRef = globalScope.document,
} = context;
let resolver = null;
let currentConfig = null;
let currentInputs = [];
function setHidden(node, hidden) {
if (!node) return;
node.hidden = Boolean(hidden);
}
function resetAlert() {
if (!alertNode) return;
alertNode.textContent = '';
alertNode.className = 'modal-alert modal-form-alert';
alertNode.hidden = true;
}
function setAlert(message = '', tone = 'danger') {
if (!alertNode) return;
const text = String(message || '').trim();
if (!text) {
resetAlert();
return;
}
alertNode.textContent = text;
alertNode.className = `modal-alert modal-form-alert${tone === 'danger' ? ' is-danger' : ''}`;
alertNode.hidden = false;
}
function close(result = null) {
if (resolver) {
resolver(result);
resolver = null;
}
currentConfig = null;
currentInputs = [];
resetAlert();
if (fieldsContainer) {
fieldsContainer.innerHTML = '';
}
if (overlay) {
overlay.hidden = true;
}
}
function buildFieldNode(field, values) {
const wrapper = documentRef.createElement('div');
wrapper.className = 'modal-form-row';
const label = documentRef.createElement('label');
label.className = 'modal-form-label';
const labelText = String(field.label || field.key || '').trim();
label.textContent = labelText;
wrapper.appendChild(label);
let input = null;
if (field.type === 'textarea') {
input = documentRef.createElement('textarea');
input.className = 'data-textarea';
} else if (field.type === 'select') {
input = documentRef.createElement('select');
input.className = 'data-select';
const options = Array.isArray(field.options) ? field.options : [];
options.forEach((option) => {
const optionNode = documentRef.createElement('option');
optionNode.value = String(option?.value || '');
optionNode.textContent = String(option?.label || option?.value || '');
input.appendChild(optionNode);
});
} else {
input = documentRef.createElement('input');
input.type = field.type === 'password' ? 'password' : 'text';
input.className = field.type === 'password'
? 'data-input data-input-with-icon'
: 'data-input';
}
const normalizedValue = Object.prototype.hasOwnProperty.call(values, field.key)
? values[field.key]
: field.value;
if (normalizedValue !== undefined && normalizedValue !== null) {
input.value = String(normalizedValue);
}
if (field.placeholder) {
input.placeholder = String(field.placeholder);
}
if (field.autocomplete) {
input.autocomplete = String(field.autocomplete);
}
if (field.inputMode) {
input.inputMode = String(field.inputMode);
}
if (field.rows && field.type === 'textarea') {
input.rows = Number(field.rows) || 3;
}
input.dataset.fieldKey = String(field.key || '');
label.htmlFor = field.key;
input.id = field.key;
if (field.type === 'password') {
const inputShell = documentRef.createElement('div');
inputShell.className = 'input-with-icon';
inputShell.appendChild(input);
const toggleButton = documentRef.createElement('button');
toggleButton.className = 'input-icon-btn';
toggleButton.type = 'button';
const labels = {
show: String(field.showPasswordLabel || `\u663e\u793a${labelText || '\u5bc6\u7801'}`),
hide: String(field.hidePasswordLabel || `\u9690\u85cf${labelText || '\u5bc6\u7801'}`),
};
syncPasswordToggleButton(toggleButton, input, labels);
toggleButton.addEventListener('click', () => {
input.type = input.type === 'password' ? 'text' : 'password';
syncPasswordToggleButton(toggleButton, input, labels);
});
inputShell.appendChild(toggleButton);
wrapper.appendChild(inputShell);
} else {
wrapper.appendChild(input);
}
if (field.type !== 'textarea') {
input.addEventListener('keydown', (event) => {
if (event.key !== 'Enter') {
return;
}
event.preventDefault();
void handleConfirm();
});
}
currentInputs.push({ field, input });
return wrapper;
}
function collectValues() {
return currentInputs.reduce((result, item) => {
result[item.field.key] = item.input.value;
return result;
}, {});
}
async function handleConfirm() {
if (!currentConfig) {
close(null);
return;
}
const values = collectValues();
resetAlert();
for (const item of currentInputs) {
const { field, input } = item;
const rawValue = values[field.key];
const textValue = String(rawValue || '').trim();
if (field.required && !textValue) {
setAlert(field.requiredMessage || `${field.label || field.key}不能为空。`);
input.focus?.();
return;
}
if (typeof field.validate === 'function') {
const validationMessage = await field.validate(rawValue, values);
if (validationMessage) {
setAlert(validationMessage);
input.focus?.();
return;
}
}
}
close(values);
}
function bindEvents() {
overlay?.addEventListener('click', (event) => {
if (event.target === overlay) {
close(null);
}
});
closeButton?.addEventListener('click', () => close(null));
cancelButton?.addEventListener('click', () => close(null));
confirmButton?.addEventListener('click', () => {
void handleConfirm();
});
}
async function open(config = {}) {
if (!overlay || !titleNode || !fieldsContainer || !confirmButton) {
return null;
}
if (resolver) {
close(null);
}
currentConfig = config || {};
currentInputs = [];
titleNode.textContent = String(currentConfig.title || '填写表单');
if (messageNode) {
const message = String(currentConfig.message || '').trim();
messageNode.textContent = message;
setHidden(messageNode, !message);
}
resetAlert();
if (currentConfig.alert?.text) {
setAlert(currentConfig.alert.text, currentConfig.alert.tone || 'danger');
}
confirmButton.textContent = String(currentConfig.confirmLabel || '确认');
confirmButton.className = `btn ${currentConfig.confirmVariant || 'btn-primary'} btn-sm`;
fieldsContainer.innerHTML = '';
const initialValues = currentConfig.initialValues && typeof currentConfig.initialValues === 'object'
? currentConfig.initialValues
: {};
const fields = Array.isArray(currentConfig.fields) ? currentConfig.fields : [];
fields.forEach((field) => {
fieldsContainer.appendChild(buildFieldNode(field, initialValues));
});
overlay.hidden = false;
const firstInput = currentInputs[0]?.input || null;
if (firstInput && typeof globalScope.requestAnimationFrame === 'function') {
globalScope.requestAnimationFrame(() => firstInput.focus?.());
} else {
firstInput?.focus?.();
}
return new Promise((resolve) => {
resolver = resolve;
});
}
bindEvents();
return {
close,
open,
};
}
globalScope.SidepanelFormDialog = {
createFormDialog,
};
})(window);
+482
View File
@@ -0,0 +1,482 @@
(function attachSidepanelHostedSmsPoolManager(globalScope) {
const SEPARATOR = '----';
function createHostedSmsPoolManager(context = {}) {
const {
dom = {},
helpers = {},
state = {},
actions = {},
constants = {},
} = context;
const copyIcon = constants.copyIcon || '';
let renderedEntries = [];
let searchTerm = '';
let filterMode = 'all';
let loading = false;
let refreshQueued = false;
function normalizeText(value = '') {
return String(value || '').trim();
}
function normalizePoolText(value = '') {
return String(value || '')
.replace(/\r/g, '')
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.join('\n');
}
function normalizePoolPhone(value = '') {
return normalizeText(value);
}
function normalizePoolUrl(value = '') {
const rawValue = normalizeText(value);
if (!rawValue) {
return '';
}
try {
const parsed = new URL(rawValue);
parsed.searchParams.delete('t');
return parsed.toString();
} catch {
return rawValue
.replace(/([?&])t=\d+(?=(&|$))/i, '$1')
.replace(/[?&]$/g, '');
}
}
function formatPayPalLocalPhone(value = '') {
const rawValue = normalizeText(value);
const digits = rawValue.replace(/\D+/g, '');
if (rawValue.startsWith('+1') && digits.length === 11 && digits.startsWith('1')) {
return digits.slice(1);
}
return digits;
}
function buildKey(phone = '', verificationUrl = '') {
const normalizedPhone = normalizePoolPhone(phone);
const normalizedUrl = normalizePoolUrl(verificationUrl);
return normalizedPhone && normalizedUrl ? `${normalizedPhone}${SEPARATOR}${normalizedUrl}` : '';
}
function parseEntries(text = '') {
const lines = normalizePoolText(text).split('\n').filter(Boolean);
const seen = new Set();
const entries = [];
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
const separatorIndex = line.indexOf(SEPARATOR);
const hasSeparator = separatorIndex > 0;
const phone = hasSeparator
? normalizePoolPhone(line.slice(0, separatorIndex))
: normalizePoolPhone(line);
const verificationUrl = hasSeparator
? normalizePoolUrl(line.slice(separatorIndex + SEPARATOR.length))
: normalizePoolUrl(lines[index + 1] || '');
if (!hasSeparator && verificationUrl) {
index += 1;
}
const key = buildKey(phone, verificationUrl);
if (!phone || !verificationUrl || !key || seen.has(key)) {
continue;
}
seen.add(key);
entries.push({
index,
key,
phone,
verificationUrl,
});
}
return entries;
}
function entriesToText(entries = []) {
return parseEntries(entries.map((entry) => `${entry.phone}${SEPARATOR}${entry.verificationUrl}`).join('\n'))
.map((entry) => `${entry.phone}${SEPARATOR}${entry.verificationUrl}`)
.join('\n');
}
function normalizeUsage(value = {}) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return {};
}
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
const usage = item && typeof item === 'object' && !Array.isArray(item) ? item : {};
const legacyUsedCount = Number(usage.usedAt) > 0 ? 1 : 0;
const useCount = Math.max(0, Math.floor(Number(usage.useCount ?? usage.usageCount ?? legacyUsedCount) || 0));
return [normalizeText(key), {
useCount,
usedAt: Math.max(0, Number(usage.usedAt) || 0),
lastAttemptAt: Math.max(0, Number(usage.lastAttemptAt) || 0),
lastError: normalizeText(usage.lastError),
}];
}).filter(([key]) => Boolean(key)));
}
function getCurrentKey() {
const current = state.getCurrentEntry?.() || null;
return normalizeText(current?.key || buildKey(current?.phone, current?.verificationUrl));
}
function getEntriesWithState(entries = renderedEntries) {
const usage = normalizeUsage(state.getUsage?.());
const currentKey = getCurrentKey();
return parseEntries(entriesToText(entries)).map((entry) => {
const itemUsage = usage[entry.key] || {};
return {
...entry,
current: Boolean(currentKey && entry.key === currentKey),
useCount: Math.max(0, Math.floor(Number(itemUsage.useCount) || 0)),
used: Math.max(0, Math.floor(Number(itemUsage.useCount) || 0)) > 0,
lastAttemptAt: Math.max(0, Number(itemUsage.lastAttemptAt) || 0),
lastError: normalizeText(itemUsage.lastError),
};
});
}
function getFilteredEntries(entries = renderedEntries) {
const normalizedSearch = normalizeText(searchTerm).toLowerCase();
return getEntriesWithState(entries).filter((entry) => {
const matchesFilter = (() => {
switch (filterMode) {
case 'current': return Boolean(entry.current);
case 'used': return Boolean(entry.used);
case 'unused': return !entry.used;
case 'error': return Boolean(entry.lastError);
default: return true;
}
})();
if (!matchesFilter) {
return false;
}
if (!normalizedSearch) {
return true;
}
return [
entry.phone,
entry.verificationUrl,
entry.current ? 'current 当前' : '',
entry.used ? 'used 已用' : 'unused 未用',
entry.lastError ? `error 异常 ${entry.lastError}` : '',
].join(' ').toLowerCase().includes(normalizedSearch);
});
}
function setLoading(nextLoading, summary = '') {
loading = Boolean(nextLoading);
[
dom.btnHostedSmsPoolRefresh,
dom.btnHostedSmsPoolClearUsed,
dom.btnHostedSmsPoolDeleteAll,
dom.btnHostedSmsPoolImport,
].forEach((button) => {
if (button) button.disabled = loading;
});
if (dom.inputHostedSmsPoolImport) {
dom.inputHostedSmsPoolImport.disabled = loading;
}
if (summary && dom.hostedSmsPoolSummary) {
dom.hostedSmsPoolSummary.textContent = summary;
}
}
function updateControls(entries = renderedEntries) {
const entriesWithState = getEntriesWithState(entries);
const usedCount = entriesWithState.filter((entry) => entry.useCount > 0).length;
if (dom.btnHostedSmsPoolClearUsed) {
dom.btnHostedSmsPoolClearUsed.disabled = loading || usedCount === 0;
}
if (dom.btnHostedSmsPoolDeleteAll) {
dom.btnHostedSmsPoolDeleteAll.disabled = loading || entriesWithState.length === 0;
}
}
function render(entries = parseEntries(state.getText?.())) {
if (!dom.hostedSmsPoolList || !dom.hostedSmsPoolSummary) {
return;
}
renderedEntries = parseEntries(entriesToText(entries));
dom.hostedSmsPoolList.innerHTML = '';
const entriesWithState = getEntriesWithState(renderedEntries);
if (!entriesWithState.length) {
dom.hostedSmsPoolList.innerHTML = '<div class="luckmail-empty">还没有 Hosted 号码,先导入一批号码再开始。</div>';
dom.hostedSmsPoolSummary.textContent = '导入 PayPal Hosted 接码号码,每行一个号码和验证码接口。';
updateControls([]);
return;
}
const usedCount = entriesWithState.filter((entry) => entry.useCount > 0).length;
const totalUseCount = entriesWithState.reduce((sum, entry) => sum + Math.max(0, Number(entry.useCount) || 0), 0);
dom.hostedSmsPoolSummary.textContent = `已加载 ${entriesWithState.length} 个号码,${usedCount} 个有使用记录,累计使用 ${totalUseCount} 次。`;
const visibleEntries = getFilteredEntries(renderedEntries);
if (!visibleEntries.length) {
dom.hostedSmsPoolList.innerHTML = '<div class="luckmail-empty">没有匹配当前筛选条件的号码。</div>';
updateControls(renderedEntries);
return;
}
for (const entry of visibleEntries) {
const item = document.createElement('div');
item.className = `luckmail-item${entry.current ? ' is-current' : ''}`;
const localPhone = formatPayPalLocalPhone(entry.phone);
item.innerHTML = `
<div class="luckmail-item-main">
<div class="luckmail-item-email-row">
<div class="luckmail-item-email hosted-sms-pool-phone">
<span>${helpers.escapeHtml?.(entry.phone) || entry.phone}</span>
${localPhone && localPhone !== entry.phone ? `<span class="hosted-sms-pool-phone-local">PayPal 填 ${helpers.escapeHtml?.(localPhone) || localPhone}</span>` : ''}
</div>
<button
class="hotmail-copy-btn"
type="button"
data-action="copy-phone"
title="复制号码"
aria-label="复制号码 ${helpers.escapeHtml?.(entry.phone) || entry.phone}"
>${copyIcon}</button>
</div>
<div class="luckmail-item-details mono">${helpers.escapeHtml?.(entry.verificationUrl) || entry.verificationUrl}</div>
<div class="luckmail-item-meta">
${entry.current ? '<span class="luckmail-tag current">当前</span>' : ''}
<span class="luckmail-tag active">使用 ${Math.max(0, Number(entry.useCount) || 0)} 次</span>
</div>
</div>
<div class="luckmail-item-actions">
<button class="btn btn-outline btn-xs" type="button" data-action="increment-usage">次数 +1</button>
<button class="btn btn-outline btn-xs" type="button" data-action="reset-usage">清零</button>
<button class="btn btn-outline btn-xs" type="button" data-action="delete">删除</button>
</div>
`;
item.querySelector('[data-action="copy-phone"]')?.addEventListener('click', async () => {
await helpers.copyTextToClipboard?.(entry.phone || '');
helpers.showToast?.('号码已复制', 'success', 1600);
});
item.querySelector('[data-action="increment-usage"]')?.addEventListener('click', async () => {
await patchPool(({ entries: entriesList, usage }) => {
const nextUsage = { ...usage };
nextUsage[entry.key] = {
...(nextUsage[entry.key] || {}),
useCount: Math.max(0, Number(nextUsage[entry.key]?.useCount) || 0) + 1,
usedAt: Date.now(),
lastAttemptAt: Math.max(0, Number(nextUsage[entry.key]?.lastAttemptAt) || 0),
lastError: normalizeText(nextUsage[entry.key]?.lastError),
};
return { entries: entriesList, usage: nextUsage };
});
});
item.querySelector('[data-action="reset-usage"]')?.addEventListener('click', async () => {
await patchPool(({ entries: entriesList, usage }) => {
const nextUsage = { ...usage };
nextUsage[entry.key] = {
...(nextUsage[entry.key] || {}),
useCount: 0,
usedAt: 0,
lastError: '',
};
return { entries: entriesList, usage: nextUsage };
});
});
item.querySelector('[data-action="delete"]')?.addEventListener('click', async () => {
const confirmed = await helpers.openConfirmModal?.({
title: '删除 Hosted 号码',
message: `确认删除 ${entry.phone} 吗?此操作不可撤销。`,
confirmLabel: '确认删除',
confirmVariant: 'btn-danger',
});
if (!confirmed) return;
await patchPool(({ entries: entriesList, usage }) => {
const nextUsage = { ...usage };
delete nextUsage[entry.key];
return {
entries: entriesList.filter((candidate) => candidate.key !== entry.key),
usage: nextUsage,
};
});
});
dom.hostedSmsPoolList.appendChild(item);
}
updateControls(renderedEntries);
}
async function patchPool(mutator) {
const previousText = normalizePoolText(state.getText?.());
const previousUsage = normalizeUsage(state.getUsage?.());
const previousEntries = parseEntries(previousText);
const result = mutator({
entries: previousEntries.map((entry) => ({ ...entry })),
usage: { ...previousUsage },
}) || {};
const nextEntries = parseEntries(entriesToText(result.entries || previousEntries));
const nextUsage = normalizeUsage(result.usage || previousUsage);
const nextText = entriesToText(nextEntries);
setLoading(true, '正在更新 Hosted 号池...');
state.setText?.(nextText);
state.setUsage?.(nextUsage);
if (!nextText) {
actions.clearFallback?.();
}
render(nextEntries);
try {
await actions.persistPool?.();
return true;
} catch (error) {
state.setText?.(previousText);
state.setUsage?.(previousUsage);
render(previousEntries);
helpers.showToast?.(`更新 Hosted 号池失败:${error.message}`, 'error');
return false;
} finally {
setLoading(false);
}
}
async function importEntries() {
const text = normalizePoolText(dom.inputHostedSmsPoolImport?.value || '');
if (!text) {
helpers.showToast?.('请先粘贴 Hosted 号码,每行一个号码和验证码接口。', 'warn');
return;
}
const previousEntries = parseEntries(state.getText?.());
const knownKeys = new Set(previousEntries.map((entry) => entry.key));
const imported = [];
let skippedCount = 0;
for (const entry of parseEntries(text)) {
if (knownKeys.has(entry.key)) {
skippedCount += 1;
continue;
}
knownKeys.add(entry.key);
imported.push(entry);
}
if (!imported.length) {
helpers.showToast?.(skippedCount > 0 ? '没有可导入的新号码(可能都重复或格式无效)。' : '没有识别到有效号码。', 'warn');
return;
}
const persisted = await patchPool(({ entries, usage }) => ({
entries: [...entries, ...imported],
usage,
}));
if (!persisted) {
return;
}
if (dom.inputHostedSmsPoolImport) {
dom.inputHostedSmsPoolImport.value = '';
}
helpers.showToast?.(
skippedCount > 0
? `已导入 ${imported.length} 个号码,跳过 ${skippedCount} 条重复数据。`
: `已导入 ${imported.length} 个号码。`,
'success',
2200
);
}
async function clearUsedState() {
const confirmed = await helpers.openConfirmModal?.({
title: '清空使用次数',
message: '确认清空 Hosted 号池的使用次数吗?号码本身会保留。',
confirmLabel: '清空次数',
});
if (!confirmed) return;
await patchPool(({ entries }) => ({ entries, usage: {} }));
}
async function deleteAll() {
const confirmed = await helpers.openConfirmModal?.({
title: '删除 Hosted 号池',
message: '确认删除当前全部 Hosted 号码吗?此操作不可撤销。',
confirmLabel: '确认删除',
confirmVariant: 'btn-danger',
});
if (!confirmed) return;
await patchPool(() => ({ entries: [], usage: {} }));
}
function refresh(options = {}) {
const { silent = false } = options;
if (state.isVisible && !state.isVisible()) {
return;
}
if (!silent) setLoading(true, '正在刷新 Hosted 号池...');
render(parseEntries(state.getText?.()));
if (!silent) setLoading(false);
}
function queueRefresh() {
if (refreshQueued) return;
refreshQueued = true;
setTimeout(() => {
refreshQueued = false;
refresh({ silent: true });
}, 120);
}
function bindEvents() {
dom.btnHostedSmsPoolRefresh?.addEventListener('click', () => refresh());
dom.btnHostedSmsPoolImport?.addEventListener('click', () => {
void importEntries();
});
dom.inputHostedSmsPoolImport?.addEventListener('keydown', (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
event.preventDefault();
void importEntries();
}
});
dom.inputHostedSmsPoolSearch?.addEventListener('input', (event) => {
searchTerm = normalizeText(event.target.value);
render(renderedEntries);
});
dom.selectHostedSmsPoolFilter?.addEventListener('change', (event) => {
filterMode = normalizeText(event.target.value) || 'all';
render(renderedEntries);
});
dom.btnHostedSmsPoolClearUsed?.addEventListener('click', () => {
void clearUsedState();
});
dom.btnHostedSmsPoolDeleteAll?.addEventListener('click', () => {
void deleteAll();
});
}
function reset() {
searchTerm = '';
filterMode = 'all';
if (dom.inputHostedSmsPoolSearch) dom.inputHostedSmsPoolSearch.value = '';
if (dom.selectHostedSmsPoolFilter) dom.selectHostedSmsPoolFilter.value = 'all';
if (dom.hostedSmsPoolList) dom.hostedSmsPoolList.innerHTML = '';
if (dom.hostedSmsPoolSummary) {
dom.hostedSmsPoolSummary.textContent = '导入 PayPal Hosted 接码号码,每行一个号码和验证码接口。';
}
updateControls([]);
}
return {
bindEvents,
queueRefresh,
refresh,
render,
reset,
};
}
globalScope.SidepanelHostedSmsPoolManager = {
createHostedSmsPoolManager,
};
})(typeof window !== 'undefined' ? window : globalThis);
+608
View File
@@ -0,0 +1,608 @@
(function attachSidepanelHotmailManager(globalScope) {
function createHotmailManager(context = {}) {
const {
state,
dom,
helpers,
runtime,
constants = {},
hotmailUtils = {},
} = context;
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;
let searchTerm = '';
let filterMode = 'all';
function getHotmailAccountsByUsage(mode = 'all', currentState = state.getLatestState()) {
const accounts = helpers.getHotmailAccounts(currentState);
if (typeof hotmailUtils.filterHotmailAccountsByUsage === 'function') {
return hotmailUtils.filterHotmailAccountsByUsage(accounts, mode);
}
if (mode === 'used') {
return accounts.filter((account) => Boolean(account?.used));
}
return accounts.slice();
}
function getHotmailBulkActionText(mode, count) {
if (typeof hotmailUtils.getHotmailBulkActionLabel === 'function') {
return hotmailUtils.getHotmailBulkActionLabel(mode, count);
}
const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0;
const prefix = mode === 'used' ? '清空已用' : '全部删除';
const suffix = normalizedCount > 0 ? `${normalizedCount}` : '';
return `${prefix}${suffix}`;
}
function getHotmailListToggleText(expanded, count) {
if (typeof hotmailUtils.getHotmailListToggleLabel === 'function') {
return hotmailUtils.getHotmailListToggleLabel(expanded, count);
}
const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0;
const suffix = normalizedCount > 0 ? `${normalizedCount}` : '';
return `${expanded ? '收起列表' : '展开列表'}${suffix}`;
}
function updateHotmailListViewport() {
const count = helpers.getHotmailAccounts().length;
const usedCount = getHotmailAccountsByUsage('used').length;
if (dom.btnClearUsedHotmailAccounts) {
dom.btnClearUsedHotmailAccounts.textContent = getHotmailBulkActionText('used', usedCount);
dom.btnClearUsedHotmailAccounts.disabled = usedCount === 0;
}
if (dom.btnDeleteAllHotmailAccounts) {
dom.btnDeleteAllHotmailAccounts.textContent = getHotmailBulkActionText('all', count);
dom.btnDeleteAllHotmailAccounts.disabled = count === 0;
}
if (dom.btnToggleHotmailList) {
dom.btnToggleHotmailList.textContent = getHotmailListToggleText(listExpanded, count);
dom.btnToggleHotmailList.setAttribute('aria-expanded', String(listExpanded));
dom.btnToggleHotmailList.disabled = count === 0;
}
if (dom.hotmailListShell) {
dom.hotmailListShell.classList.toggle('is-expanded', listExpanded);
dom.hotmailListShell.classList.toggle('is-collapsed', !listExpanded);
}
}
function setHotmailListExpanded(expanded, options = {}) {
const { persist = true } = options;
listExpanded = Boolean(expanded);
updateHotmailListViewport();
if (persist) {
localStorage.setItem(expandedStorageKey, listExpanded ? '1' : '0');
}
}
function initHotmailListExpandedState() {
const saved = localStorage.getItem(expandedStorageKey);
setHotmailListExpanded(saved === '1', { persist: false });
}
function shouldClearCurrentHotmailSelectionLocally(account) {
if (typeof hotmailUtils.shouldClearHotmailCurrentSelection === 'function') {
return hotmailUtils.shouldClearHotmailCurrentSelection(account);
}
return Boolean(account) && account.used === true;
}
function upsertHotmailAccountListLocally(accounts, nextAccount) {
if (typeof hotmailUtils.upsertHotmailAccountInList === 'function') {
return hotmailUtils.upsertHotmailAccountInList(accounts, nextAccount);
}
const list = Array.isArray(accounts) ? accounts.slice() : [];
if (!nextAccount?.id) return list;
const existingIndex = list.findIndex((account) => account?.id === nextAccount.id);
if (existingIndex === -1) {
list.push(nextAccount);
return list;
}
list[existingIndex] = nextAccount;
return list;
}
function refreshHotmailSelectionUI() {
renderHotmailAccounts();
if (dom.selectMailProvider.value === 'hotmail-api') {
dom.inputEmail.value = helpers.getCurrentHotmailEmail();
}
}
function applyHotmailAccountMutation(account, options = {}) {
if (!account?.id) return;
const { preserveCurrentSelection = false } = options;
const latestState = state.getLatestState();
const nextState = {
hotmailAccounts: upsertHotmailAccountListLocally(helpers.getHotmailAccounts(), account),
};
if (!preserveCurrentSelection
&& latestState?.currentHotmailAccountId === account.id
&& shouldClearCurrentHotmailSelectionLocally(account)) {
nextState.currentHotmailAccountId = null;
if (dom.selectMailProvider.value === 'hotmail-api') {
nextState.email = null;
}
}
state.syncLatestState(nextState);
refreshHotmailSelectionUI();
}
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 getHotmailAvailabilityLabel(account) {
if (account.used) return '已用';
return '可分配';
}
function getHotmailStatusLabel(account) {
if (account.used) return '已用';
switch (account.status) {
case 'authorized':
return '可用';
case 'error':
return '异常';
default:
return '待校验';
}
}
function getHotmailStatusClass(account) {
if (account.used) return 'status-used';
return `status-${account.status || 'pending'}`;
}
function normalizeSearchText(value = '') {
return String(value || '').trim().toLowerCase();
}
function getFilteredHotmailAccounts(accounts, currentId = '') {
const normalizedSearchTerm = normalizeSearchText(searchTerm);
return accounts.filter((account) => {
const isCurrent = Boolean(currentId) && account.id === currentId;
const matchesFilter = (() => {
switch (filterMode) {
case 'current': return isCurrent;
case 'available': return !account.used;
case 'used': return Boolean(account.used);
case 'error': return account.status === 'error';
default: return true;
}
})();
if (!matchesFilter) return false;
if (!normalizedSearchTerm) return true;
const haystack = [
account.email,
account.status,
getHotmailAvailabilityLabel(account),
getHotmailStatusLabel(account),
isCurrent ? 'current 当前' : '',
].join(' ').toLowerCase();
return haystack.includes(normalizedSearchTerm);
});
}
function clearHotmailForm() {
dom.inputHotmailEmail.value = '';
dom.inputHotmailClientId.value = '';
dom.inputHotmailPassword.value = '';
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();
const accounts = helpers.getHotmailAccounts();
const currentId = latestState?.currentHotmailAccountId || '';
if (!accounts.length) {
dom.hotmailAccountsList.innerHTML = '<div class="hotmail-empty">还没有 Hotmail 账号,先添加一条再校验。</div>';
updateHotmailListViewport();
return;
}
const visibleAccounts = getFilteredHotmailAccounts(accounts, currentId);
if (!visibleAccounts.length) {
dom.hotmailAccountsList.innerHTML = '<div class="hotmail-empty">没有匹配当前筛选条件的 Hotmail 账号。</div>';
updateHotmailListViewport();
return;
}
dom.hotmailAccountsList.innerHTML = visibleAccounts.map((account) => `
<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(getHotmailStatusClass(account))}">${helpers.escapeHtml(getHotmailStatusLabel(account))}</span>
</div>
<div class="hotmail-account-meta">
<span>客户端 ID${helpers.escapeHtml(account.clientId ? `${account.clientId.slice(0, 10)}...` : '未填写')}</span>
<span>刷新令牌:${account.refreshToken ? '已保存' : '未保存'}</span>
<span>分配状态: ${helpers.escapeHtml(getHotmailAvailabilityLabel(account))}</span>
<span>上次校验: ${helpers.escapeHtml(formatDateTime(account.lastAuthAt))}</span>
<span>上次使用: ${helpers.escapeHtml(formatDateTime(account.lastUsedAt))}</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-outline btn-sm" type="button" data-account-action="toggle-used" data-account-id="${helpers.escapeHtml(account.id)}">${account.used ? '标记未用' : '标记已用'}</button>
<button class="btn btn-primary btn-sm" type="button" data-account-action="verify" data-account-id="${helpers.escapeHtml(account.id)}">校验</button>
<button class="btn btn-outline btn-sm" type="button" data-account-action="test" 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('');
updateHotmailListViewport();
}
async function deleteHotmailAccountsByMode(mode) {
const isUsedMode = mode === 'used';
const targetAccounts = getHotmailAccountsByUsage(isUsedMode ? 'used' : 'all');
if (!targetAccounts.length) {
helpers.showToast(isUsedMode ? '没有已用账号可清空。' : '没有可删除的 Hotmail 账号。', 'warn');
return;
}
const confirmed = await helpers.openConfirmModal({
title: isUsedMode ? '清空已用账号' : '全部删除账号',
message: isUsedMode
? `确认删除当前 ${targetAccounts.length} 个已用 Hotmail 账号吗?`
: `确认删除全部 ${targetAccounts.length} 个 Hotmail 账号吗?`,
confirmLabel: isUsedMode ? '确认清空已用' : '确认全部删除',
confirmVariant: isUsedMode ? 'btn-outline' : 'btn-danger',
});
if (!confirmed) {
return;
}
const response = await runtime.sendMessage({
type: 'DELETE_HOTMAIL_ACCOUNTS',
source: 'sidepanel',
payload: { mode: isUsedMode ? 'used' : 'all' },
});
if (response?.error) {
throw new Error(response.error);
}
const latestState = state.getLatestState();
const targetIds = new Set(targetAccounts.map((account) => account.id));
const nextAccounts = isUsedMode
? helpers.getHotmailAccounts().filter((account) => !targetIds.has(account.id))
: [];
const nextState = { hotmailAccounts: nextAccounts };
if (latestState?.currentHotmailAccountId && targetIds.has(latestState.currentHotmailAccountId)) {
nextState.currentHotmailAccountId = null;
if (dom.selectMailProvider.value === 'hotmail-api') {
nextState.email = null;
}
}
state.syncLatestState(nextState);
refreshHotmailSelectionUI();
helpers.showToast(
isUsedMode
? `已清空 ${response.deletedCount || 0} 个已用 Hotmail 账号`
: `已删除全部 ${response.deletedCount || 0} 个 Hotmail 账号`,
'success',
2200
);
}
async function handleAddHotmailAccount() {
if (actionInFlight) return;
const email = dom.inputHotmailEmail.value.trim();
const clientId = dom.inputHotmailClientId.value.trim();
const refreshToken = dom.inputHotmailRefreshToken.value.trim();
if (!email) {
helpers.showToast('请先填写 Hotmail 邮箱。', 'warn');
return;
}
if (!clientId) {
helpers.showToast('请先填写微软应用客户端 ID。', 'warn');
return;
}
if (!refreshToken) {
helpers.showToast('请先填写刷新令牌(refresh token)。', 'warn');
return;
}
actionInFlight = true;
dom.btnAddHotmailAccount.disabled = true;
try {
const response = await runtime.sendMessage({
type: 'UPSERT_HOTMAIL_ACCOUNT',
source: 'sidepanel',
payload: {
email,
clientId,
password: dom.inputHotmailPassword.value,
refreshToken,
},
});
if (response?.error) {
throw new Error(response.error);
}
helpers.showToast(`已保存 Hotmail 账号 ${email}`, 'success', 1800);
formController.setVisible(false, { clearForm: true });
} catch (err) {
helpers.showToast(`保存 Hotmail 账号失败:${err.message}`, 'error');
} finally {
actionInFlight = false;
dom.btnAddHotmailAccount.disabled = false;
}
}
async function handleImportHotmailAccounts() {
if (actionInFlight) return;
if (typeof hotmailUtils.parseHotmailImportText !== 'function') {
helpers.showToast('导入解析器未加载,请刷新扩展后重试。', 'error');
return;
}
const rawText = dom.inputHotmailImport.value.trim();
if (!rawText) {
helpers.showToast('请先粘贴账号导入内容。', 'warn');
return;
}
const parsedAccounts = hotmailUtils.parseHotmailImportText(rawText);
if (!parsedAccounts.length) {
helpers.showToast('没有解析到有效账号,请检查格式是否为 账号----密码----ID----Token。', 'error');
return;
}
actionInFlight = true;
dom.btnImportHotmailAccounts.disabled = true;
try {
for (const account of parsedAccounts) {
const response = await runtime.sendMessage({
type: 'UPSERT_HOTMAIL_ACCOUNT',
source: 'sidepanel',
payload: account,
});
if (response?.error) {
throw new Error(response.error);
}
}
dom.inputHotmailImport.value = '';
helpers.showToast(`已导入 ${parsedAccounts.length} 条 Hotmail 账号`, 'success', 2200);
} catch (err) {
helpers.showToast(`批量导入失败:${err.message}`, 'error');
} finally {
actionInFlight = false;
dom.btnImportHotmailAccounts.disabled = false;
}
}
async function handleAccountListClick(event) {
const actionButton = event.target.closest('[data-account-action]');
if (!actionButton || actionInFlight) {
return;
}
const accountId = actionButton.dataset.accountId;
const action = actionButton.dataset.accountAction;
if (!accountId || !action) {
return;
}
const targetAccount = helpers.getHotmailAccounts().find((account) => account.id === accountId) || null;
actionInFlight = true;
actionButton.disabled = true;
try {
if (action === 'copy-email') {
if (!targetAccount?.email) throw new Error('未找到可复制的邮箱地址。');
await helpers.copyTextToClipboard(targetAccount.email);
helpers.showToast(`已复制 ${targetAccount.email}`, 'success', 1800);
} else if (action === 'select') {
const response = await runtime.sendMessage({
type: 'SELECT_HOTMAIL_ACCOUNT',
source: 'sidepanel',
payload: { accountId },
});
if (response?.error) throw new Error(response.error);
state.syncLatestState({ currentHotmailAccountId: response.account.id });
applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
helpers.showToast(`已切换当前 Hotmail 账号为 ${response.account.email}`, 'success', 1800);
} else if (action === 'toggle-used') {
if (!targetAccount) throw new Error('未找到目标 Hotmail 账号。');
const response = await runtime.sendMessage({
type: 'PATCH_HOTMAIL_ACCOUNT',
source: 'sidepanel',
payload: {
accountId,
updates: { used: !targetAccount.used },
},
});
if (response?.error) throw new Error(response.error);
applyHotmailAccountMutation(response.account);
helpers.showToast(`账号 ${response.account.email}${response.account.used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
} else if (action === 'verify') {
const response = await runtime.sendMessage({
type: 'VERIFY_HOTMAIL_ACCOUNT',
source: 'sidepanel',
payload: { accountId },
});
if (response?.error) throw new Error(response.error);
applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
helpers.showToast(`账号 ${response.account.email} 校验通过`, 'success', 2200);
} else if (action === 'test') {
const response = await runtime.sendMessage({
type: 'TEST_HOTMAIL_ACCOUNT',
source: 'sidepanel',
payload: { accountId },
});
if (response?.error) throw new Error(response.error);
applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
if (response.latestCode) {
await helpers.copyTextToClipboard(response.latestCode);
const mailbox = response.latestMailbox ? `${response.latestMailbox}` : '';
helpers.showToast(`已复制最新验证码 ${response.latestCode}${mailbox}`, 'success', 2600);
} else if (response.latestSubject) {
const mailbox = response.latestMailbox ? `${response.latestMailbox}` : '';
helpers.showToast(`最新邮件${mailbox}没有验证码:${response.latestSubject}`, 'warn', 3200);
} else {
helpers.showToast('当前没有可读取的最新邮件。', 'warn', 2600);
}
} else if (action === 'delete') {
const confirmed = await helpers.openConfirmModal({
title: '删除账号',
message: '确认删除这个 Hotmail 账号吗?对应 token 也会一起移除。',
confirmLabel: '确认删除',
confirmVariant: 'btn-danger',
});
if (!confirmed) {
return;
}
const response = await runtime.sendMessage({
type: 'DELETE_HOTMAIL_ACCOUNT',
source: 'sidepanel',
payload: { accountId },
});
if (response?.error) throw new Error(response.error);
helpers.showToast('Hotmail 账号已删除', 'success', 1800);
}
} catch (err) {
helpers.showToast(err.message, 'error');
} finally {
actionInFlight = false;
actionButton.disabled = false;
}
}
function bindHotmailEvents() {
dom.btnToggleHotmailList?.addEventListener('click', () => {
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: '使用教程',
message: 'API对接模式会直接调用微软邮箱接口取件;本地助手模式仍走本地服务。两种模式继续共用同一套 Hotmail 账号池与导入格式。',
confirmLabel: '确定',
confirmVariant: 'btn-primary',
});
});
dom.btnClearUsedHotmailAccounts?.addEventListener('click', async () => {
if (actionInFlight) return;
actionInFlight = true;
dom.btnClearUsedHotmailAccounts.disabled = true;
try {
await deleteHotmailAccountsByMode('used');
} catch (err) {
helpers.showToast(err.message, 'error');
} finally {
actionInFlight = false;
updateHotmailListViewport();
}
});
dom.btnDeleteAllHotmailAccounts?.addEventListener('click', async () => {
if (actionInFlight) return;
actionInFlight = true;
dom.btnDeleteAllHotmailAccounts.disabled = true;
try {
await deleteHotmailAccountsByMode('all');
} catch (err) {
helpers.showToast(err.message, 'error');
} finally {
actionInFlight = false;
updateHotmailListViewport();
}
});
dom.btnAddHotmailAccount?.addEventListener('click', handleAddHotmailAccount);
dom.btnImportHotmailAccounts?.addEventListener('click', handleImportHotmailAccounts);
dom.inputHotmailSearch?.addEventListener('input', (event) => {
searchTerm = normalizeSearchText(event.target.value);
renderHotmailAccounts();
});
dom.selectHotmailFilter?.addEventListener('change', (event) => {
filterMode = String(event.target.value || 'all');
renderHotmailAccounts();
});
dom.hotmailAccountsList?.addEventListener('click', handleAccountListClick);
formController.sync();
}
return {
bindHotmailEvents,
initHotmailListExpandedState,
renderHotmailAccounts,
};
}
globalScope.SidepanelHotmailManager = {
createHotmailManager,
};
})(window);
+522
View File
@@ -0,0 +1,522 @@
(function attachSidepanelIcloudManager(globalScope) {
function createIcloudManager(context = {}) {
const {
dom,
helpers,
runtime,
} = context;
let refreshQueued = false;
let renderedAliases = [];
let selectedEmails = new Set();
let searchTerm = '';
let filterMode = 'all';
function normalizeIcloudSearchText(value) {
return String(value || '').trim().toLowerCase();
}
function getFilteredIcloudAliases(aliases = renderedAliases) {
const normalizedSearchTerm = normalizeIcloudSearchText(searchTerm);
return (Array.isArray(aliases) ? aliases : []).filter((alias) => {
const matchesFilter = (() => {
switch (filterMode) {
case 'active': return Boolean(alias.active);
case 'used': return Boolean(alias.used);
case 'unused': return !alias.used;
case 'preserved': return Boolean(alias.preserved);
default: return true;
}
})();
if (!matchesFilter) return false;
if (!normalizedSearchTerm) return true;
const haystack = [
alias.email,
alias.label,
alias.note,
alias.used ? '已用 used' : '未用 unused',
alias.active ? '可用 active' : '不可用 inactive',
alias.preserved ? '保留 preserved' : '',
].join(' ').toLowerCase();
return haystack.includes(normalizedSearchTerm);
});
}
function pruneIcloudSelection(aliases = renderedAliases) {
const existing = new Set((Array.isArray(aliases) ? aliases : []).map((alias) => alias.email));
selectedEmails = new Set([...selectedEmails].filter((email) => existing.has(email)));
}
function updateIcloudBulkUI(visibleAliases = getFilteredIcloudAliases()) {
if (!dom.checkboxIcloudSelectAll || !dom.icloudSelectionSummary) {
return;
}
const visibleEmails = visibleAliases.map((alias) => alias.email);
const selectedVisibleCount = visibleEmails.filter((email) => selectedEmails.has(email)).length;
const hasVisible = visibleEmails.length > 0;
dom.checkboxIcloudSelectAll.checked = hasVisible && selectedVisibleCount === visibleEmails.length;
dom.checkboxIcloudSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleEmails.length;
dom.checkboxIcloudSelectAll.disabled = !hasVisible;
dom.icloudSelectionSummary.textContent = `已选 ${selectedEmails.size} 个(当前显示 ${visibleEmails.length} 个)`;
const hasSelection = selectedEmails.size > 0;
if (dom.btnIcloudBulkUsed) dom.btnIcloudBulkUsed.disabled = !hasSelection;
if (dom.btnIcloudBulkUnused) dom.btnIcloudBulkUnused.disabled = !hasSelection;
if (dom.btnIcloudBulkPreserve) dom.btnIcloudBulkPreserve.disabled = !hasSelection;
if (dom.btnIcloudBulkUnpreserve) dom.btnIcloudBulkUnpreserve.disabled = !hasSelection;
if (dom.btnIcloudBulkDelete) dom.btnIcloudBulkDelete.disabled = !hasSelection;
}
function setIcloudLoadingState(loading, summary = '') {
if (dom.btnIcloudRefresh) dom.btnIcloudRefresh.disabled = loading;
if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = loading;
if (dom.btnIcloudLoginDone) dom.btnIcloudLoginDone.disabled = loading;
if (dom.inputIcloudSearch) dom.inputIcloudSearch.disabled = loading;
if (dom.selectIcloudFilter) dom.selectIcloudFilter.disabled = loading;
if (dom.checkboxIcloudSelectAll) dom.checkboxIcloudSelectAll.disabled = loading || getFilteredIcloudAliases().length === 0;
if (dom.btnIcloudBulkUsed) dom.btnIcloudBulkUsed.disabled = loading || selectedEmails.size === 0;
if (dom.btnIcloudBulkUnused) dom.btnIcloudBulkUnused.disabled = loading || selectedEmails.size === 0;
if (dom.btnIcloudBulkPreserve) dom.btnIcloudBulkPreserve.disabled = loading || selectedEmails.size === 0;
if (dom.btnIcloudBulkUnpreserve) dom.btnIcloudBulkUnpreserve.disabled = loading || selectedEmails.size === 0;
if (dom.btnIcloudBulkDelete) dom.btnIcloudBulkDelete.disabled = loading || selectedEmails.size === 0;
if (summary && dom.icloudSummary) dom.icloudSummary.textContent = summary;
}
function showIcloudLoginHelp(payload = {}) {
if (!dom.icloudLoginHelp) return;
const loginUrl = String(payload.loginUrl || '').trim();
let host = 'icloud.com.cn / icloud.com';
if (loginUrl) {
try {
host = new URL(loginUrl).host;
} catch {
host = loginUrl;
}
}
if (dom.icloudLoginHelpTitle) dom.icloudLoginHelpTitle.textContent = '需要登录 iCloud';
if (dom.icloudLoginHelpText) dom.icloudLoginHelpText.textContent = `我已经为你打开 ${host}。请在那个页面完成登录,然后回到这里点击“我已登录”。`;
dom.icloudLoginHelp.style.display = 'flex';
}
function hideIcloudLoginHelp() {
if (dom.icloudLoginHelp) {
dom.icloudLoginHelp.style.display = 'none';
}
}
function renderIcloudAliases(aliases = []) {
if (!dom.icloudList || !dom.icloudSummary) return;
renderedAliases = Array.isArray(aliases) ? aliases : [];
pruneIcloudSelection(renderedAliases);
dom.icloudList.innerHTML = '';
if (!aliases.length) {
selectedEmails.clear();
dom.icloudList.innerHTML = '<div class="icloud-empty">未找到 iCloud Hide My Email 别名。</div>';
dom.icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。';
if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = true;
updateIcloudBulkUI([]);
return;
}
const usedCount = aliases.filter((alias) => alias.used).length;
const deletableUsedCount = aliases.filter((alias) => alias.used && !alias.preserved).length;
dom.icloudSummary.textContent = `已加载 ${aliases.length} 个别名,其中 ${usedCount} 个已标记为已用。`;
if (dom.btnIcloudDeleteUsed) dom.btnIcloudDeleteUsed.disabled = deletableUsedCount === 0;
const visibleAliases = getFilteredIcloudAliases(aliases);
if (!visibleAliases.length) {
dom.icloudList.innerHTML = '<div class="icloud-empty">没有匹配当前筛选条件的别名。</div>';
updateIcloudBulkUI([]);
return;
}
for (const alias of visibleAliases) {
const item = document.createElement('div');
item.className = 'icloud-item';
item.innerHTML = `
<input class="icloud-item-check" type="checkbox" data-action="select" ${selectedEmails.has(alias.email) ? 'checked' : ''} />
<div class="icloud-item-main">
<div class="icloud-item-email">${helpers.escapeHtml(alias.email)}</div>
<div class="icloud-item-meta">
${alias.used ? '<span class="icloud-tag used">已用</span>' : ''}
${!alias.used && alias.active ? '<span class="icloud-tag active">可用</span>' : ''}
${alias.preserved ? '<span class="icloud-tag">保留</span>' : ''}
${alias.label ? `<span class="icloud-tag">${helpers.escapeHtml(alias.label)}</span>` : ''}
${alias.note ? `<span class="icloud-tag">${helpers.escapeHtml(alias.note)}</span>` : ''}
</div>
</div>
<div class="icloud-item-actions">
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-used">${helpers.escapeHtml(alias.used ? '标记未用' : '标记已用')}</button>
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-preserved">${helpers.escapeHtml(alias.preserved ? '取消保留' : '保留')}</button>
<button class="btn btn-outline btn-xs" type="button" data-action="delete">删除</button>
</div>
`;
item.querySelector('[data-action="select"]').addEventListener('change', (event) => {
if (event.target.checked) {
selectedEmails.add(alias.email);
} else {
selectedEmails.delete(alias.email);
}
updateIcloudBulkUI(visibleAliases);
});
item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => {
await setSingleIcloudAliasUsedState(alias, !alias.used);
});
item.querySelector('[data-action="toggle-preserved"]').addEventListener('click', async () => {
await setSingleIcloudAliasPreservedState(alias, !alias.preserved);
});
item.querySelector('[data-action="delete"]').addEventListener('click', async () => {
await deleteSingleIcloudAlias(alias);
});
dom.icloudList.appendChild(item);
}
updateIcloudBulkUI(visibleAliases);
}
async function refreshIcloudAliases(options = {}) {
const { silent = false } = options;
if (!dom.icloudSection || dom.icloudSection.style.display === 'none') {
return;
}
if (!silent) setIcloudLoadingState(true, '正在加载 iCloud 别名...');
try {
const response = await runtime.sendMessage({
type: 'LIST_ICLOUD_ALIASES',
source: 'sidepanel',
payload: {},
});
if (response?.error) throw new Error(response.error);
hideIcloudLoginHelp();
renderIcloudAliases(response?.aliases || []);
} catch (err) {
selectedEmails.clear();
if (dom.icloudList) {
dom.icloudList.innerHTML = '<div class="icloud-empty">无法加载 iCloud 别名。</div>';
}
if (dom.icloudSummary) {
dom.icloudSummary.textContent = err.message;
}
updateIcloudBulkUI([]);
if (!silent) helpers.showToast(`iCloud 别名加载失败:${err.message}`, 'error');
} finally {
setIcloudLoadingState(false);
}
}
function queueIcloudAliasRefresh() {
if (refreshQueued) return;
refreshQueued = true;
setTimeout(async () => {
refreshQueued = false;
await refreshIcloudAliases({ silent: true });
}, 150);
}
async function deleteSingleIcloudAlias(alias) {
const confirmed = await helpers.openConfirmModal({
title: '删除 iCloud 别名',
message: `确认删除 ${alias.email} 吗?此操作不可撤销。`,
confirmLabel: '确认删除',
confirmVariant: 'btn-danger',
});
if (!confirmed) {
return;
}
setIcloudLoadingState(true, `正在删除 ${alias.email} ...`);
try {
const response = await runtime.sendMessage({
type: 'DELETE_ICLOUD_ALIAS',
source: 'sidepanel',
payload: { email: alias.email, anonymousId: alias.anonymousId },
});
if (response?.error) throw new Error(response.error);
helpers.showToast(`已删除 ${alias.email}`, 'success', 2200);
await refreshIcloudAliases({ silent: true });
} catch (err) {
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
helpers.showToast(`删除 iCloud 别名失败:${err.message}`, 'error');
} finally {
setIcloudLoadingState(false);
}
}
async function setSingleIcloudAliasUsedState(alias, used) {
setIcloudLoadingState(true, `正在更新 ${alias.email} 的使用状态...`);
try {
const response = await runtime.sendMessage({
type: 'SET_ICLOUD_ALIAS_USED_STATE',
source: 'sidepanel',
payload: { email: alias.email, used },
});
if (response?.error) throw new Error(response.error);
helpers.showToast(`${alias.email}${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
await refreshIcloudAliases({ silent: true });
} catch (err) {
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
helpers.showToast(`更新 iCloud 使用状态失败:${err.message}`, 'error');
} finally {
setIcloudLoadingState(false);
}
}
async function setSingleIcloudAliasPreservedState(alias, preserved) {
setIcloudLoadingState(true, `正在更新 ${alias.email} 的保留状态...`);
try {
const response = await runtime.sendMessage({
type: 'SET_ICLOUD_ALIAS_PRESERVED_STATE',
source: 'sidepanel',
payload: { email: alias.email, preserved },
});
if (response?.error) throw new Error(response.error);
helpers.showToast(`${alias.email}${preserved ? '设为保留' : '取消保留'}`, 'success', 2200);
await refreshIcloudAliases({ silent: true });
} catch (err) {
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
helpers.showToast(`更新 iCloud 保留状态失败:${err.message}`, 'error');
} finally {
setIcloudLoadingState(false);
}
}
async function runBulkIcloudAction(action) {
const selectedAliases = renderedAliases.filter((alias) => selectedEmails.has(alias.email));
if (!selectedAliases.length) {
updateIcloudBulkUI();
return;
}
if (action === 'delete') {
const confirmed = await helpers.openConfirmModal({
title: '批量删除 iCloud 别名',
message: `确认删除选中的 ${selectedAliases.length} 个 iCloud 别名吗?此操作不可撤销。`,
confirmLabel: '确认删除',
confirmVariant: 'btn-danger',
});
if (!confirmed) {
return;
}
}
const actionLabelMap = {
used: '标记已用',
unused: '标记未用',
preserve: '保留',
unpreserve: '取消保留',
delete: '删除',
};
setIcloudLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} iCloud 别名...`);
try {
for (const alias of selectedAliases) {
let response = null;
if (action === 'used' || action === 'unused') {
response = await runtime.sendMessage({
type: 'SET_ICLOUD_ALIAS_USED_STATE',
source: 'sidepanel',
payload: { email: alias.email, used: action === 'used' },
});
} else if (action === 'preserve' || action === 'unpreserve') {
response = await runtime.sendMessage({
type: 'SET_ICLOUD_ALIAS_PRESERVED_STATE',
source: 'sidepanel',
payload: { email: alias.email, preserved: action === 'preserve' },
});
} else if (action === 'delete') {
response = await runtime.sendMessage({
type: 'DELETE_ICLOUD_ALIAS',
source: 'sidepanel',
payload: { email: alias.email, anonymousId: alias.anonymousId },
});
selectedEmails.delete(alias.email);
}
if (response?.error) {
throw new Error(response.error);
}
}
helpers.showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedAliases.length} 个 iCloud 别名`, 'success', 2400);
await refreshIcloudAliases({ silent: true });
} catch (err) {
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
helpers.showToast(`批量处理 iCloud 别名失败:${err.message}`, 'error');
} finally {
setIcloudLoadingState(false);
updateIcloudBulkUI();
}
}
async function deleteUsedIcloudAliases() {
const confirmed = await helpers.openConfirmModal({
title: '删除已用 iCloud 别名',
message: '确认删除所有未保留的已用 iCloud 别名吗?此操作不可撤销。',
confirmLabel: '确认删除',
confirmVariant: 'btn-danger',
});
if (!confirmed) {
return;
}
setIcloudLoadingState(true, '正在删除已用 iCloud 别名...');
try {
const response = await runtime.sendMessage({
type: 'DELETE_USED_ICLOUD_ALIASES',
source: 'sidepanel',
payload: {},
});
if (response?.error) throw new Error(response.error);
const deleted = response?.deleted || [];
const skipped = response?.skipped || [];
helpers.showToast(`已删除 ${deleted.length} 个已用别名,跳过 ${skipped.length}`, skipped.length ? 'warn' : 'success', 2800);
await refreshIcloudAliases({ silent: true });
} catch (err) {
if (dom.icloudSummary) dom.icloudSummary.textContent = err.message;
helpers.showToast(`删除已用 iCloud 别名失败:${err.message}`, 'error');
} finally {
setIcloudLoadingState(false);
}
}
function isLikelyIcloudLoginRequiredMessage(message = '') {
const lower = String(message || '').toLowerCase();
return lower.includes('请先在新打开的 icloud 页面中完成登录')
|| lower.includes('请先在当前浏览器登录')
|| lower.includes('需要先登录')
|| lower.includes('请先登录')
|| lower.includes('please sign in')
|| lower.includes('sign in required')
|| lower.includes('not logged in')
|| lower.includes('authentication required')
|| lower.includes('unauthenticated');
}
async function handleLoginDone() {
if (dom.btnIcloudLoginDone) {
dom.btnIcloudLoginDone.disabled = true;
}
try {
const response = await runtime.sendMessage({
type: 'CHECK_ICLOUD_SESSION',
source: 'sidepanel',
payload: {},
});
if (response?.error) {
throw new Error(response.error);
}
hideIcloudLoginHelp();
helpers.showToast('iCloud 会话已恢复,别名列表已刷新。', 'success', 2600);
await refreshIcloudAliases({ silent: true });
} catch (err) {
const errorMessage = String(err?.message || '未知错误');
if (isLikelyIcloudLoginRequiredMessage(errorMessage)) {
helpers.showToast(`看起来还没有登录完成:${errorMessage}`, 'warn', 4200);
return;
}
await refreshIcloudAliases({ silent: true }).catch(() => { });
helpers.showToast(`iCloud 会话校验失败(非登录态):${errorMessage}`, 'warn', 4200);
} finally {
if (dom.btnIcloudLoginDone) {
dom.btnIcloudLoginDone.disabled = false;
}
}
}
function reset() {
selectedEmails.clear();
renderedAliases = [];
searchTerm = '';
filterMode = 'all';
refreshQueued = false;
if (dom.inputIcloudSearch) dom.inputIcloudSearch.value = '';
if (dom.selectIcloudFilter) dom.selectIcloudFilter.value = 'all';
if (dom.icloudList) dom.icloudList.innerHTML = '';
if (dom.icloudSummary) dom.icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。';
updateIcloudBulkUI([]);
hideIcloudLoginHelp();
}
function hasDeletableUsedAliases() {
return renderedAliases.some((alias) => alias.used && !alias.preserved);
}
function bindIcloudEvents() {
dom.btnIcloudRefresh?.addEventListener('click', async () => {
await refreshIcloudAliases();
});
dom.btnIcloudDeleteUsed?.addEventListener('click', async () => {
await deleteUsedIcloudAliases();
});
dom.inputIcloudSearch?.addEventListener('input', () => {
searchTerm = dom.inputIcloudSearch.value || '';
renderIcloudAliases(renderedAliases);
});
dom.selectIcloudFilter?.addEventListener('change', () => {
filterMode = dom.selectIcloudFilter.value || 'all';
renderIcloudAliases(renderedAliases);
});
dom.checkboxIcloudSelectAll?.addEventListener('change', () => {
const visibleAliases = getFilteredIcloudAliases();
if (dom.checkboxIcloudSelectAll.checked) {
visibleAliases.forEach((alias) => selectedEmails.add(alias.email));
} else {
visibleAliases.forEach((alias) => selectedEmails.delete(alias.email));
}
renderIcloudAliases(renderedAliases);
});
dom.btnIcloudBulkUsed?.addEventListener('click', async () => {
await runBulkIcloudAction('used');
});
dom.btnIcloudBulkUnused?.addEventListener('click', async () => {
await runBulkIcloudAction('unused');
});
dom.btnIcloudBulkPreserve?.addEventListener('click', async () => {
await runBulkIcloudAction('preserve');
});
dom.btnIcloudBulkUnpreserve?.addEventListener('click', async () => {
await runBulkIcloudAction('unpreserve');
});
dom.btnIcloudBulkDelete?.addEventListener('click', async () => {
await runBulkIcloudAction('delete');
});
dom.btnIcloudLoginDone?.addEventListener('click', handleLoginDone);
}
return {
bindIcloudEvents,
hideIcloudLoginHelp,
hasDeletableUsedAliases,
queueIcloudAliasRefresh,
refreshIcloudAliases,
renderIcloudAliases,
reset,
showIcloudLoginHelp,
updateIcloudBulkUI,
};
}
globalScope.SidepanelIcloudManager = {
createIcloudManager,
};
})(window);
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
// sidepanel/ip-proxy-provider-711proxy.js — 711Proxy 面板专属逻辑
function normalizeIpProxyCountryCode(value = '') {
const raw = String(value || '').trim().toUpperCase().replace(/[^A-Z]/g, '');
return /^[A-Z]{2}$/.test(raw) ? raw : '';
}
function infer711RegionFromHost(host = '') {
const text = String(host || '').trim().toLowerCase().replace(/\.$/, '');
if (!text || !text.includes('.')) {
return '';
}
const firstLabel = String(text.split('.')[0] || '').trim();
return /^[a-z]{2}$/.test(firstLabel) ? firstLabel.toUpperCase() : '';
}
function infer711RegionFromUsername(username = '') {
const text = String(username || '').trim();
if (!text) {
return '';
}
const match = text.match(/(?:^|[-_])region[-_:]?([A-Za-z]{2})\b/i);
return normalizeIpProxyCountryCode(match ? match[1] : '');
}
function resolve711ProxyRegionFromInputs({ host = '', username = '', region = '' } = {}) {
const fromUsername = infer711RegionFromUsername(username);
if (fromUsername) {
return fromUsername;
}
const fromHost = infer711RegionFromHost(host);
if (fromHost) {
return fromHost;
}
return normalizeIpProxyCountryCode(region);
}
+467
View File
@@ -0,0 +1,467 @@
(function attachSidepanelLuckmailManager(globalScope) {
function createLuckmailManager(context = {}) {
const {
dom,
helpers,
runtime,
constants = {},
} = context;
const copyIcon = constants.copyIcon || '';
let renderedPurchases = [];
let selectedPurchaseIds = new Set();
let searchTerm = '';
let filterMode = 'all';
let refreshQueued = false;
function normalizeLuckmailSearchText(value) {
return String(value || '').trim().toLowerCase();
}
function getFilteredLuckmailPurchases(purchases = renderedPurchases) {
const normalizedSearchTerm = normalizeLuckmailSearchText(searchTerm);
return (Array.isArray(purchases) ? purchases : []).filter((purchase) => {
const matchesFilter = (() => {
switch (filterMode) {
case 'reusable': return Boolean(purchase.reusable);
case 'used': return Boolean(purchase.used);
case 'unused': return !purchase.used;
case 'preserved': return Boolean(purchase.preserved);
case 'disabled': return Boolean(purchase.disabled);
default: return true;
}
})();
if (!matchesFilter) return false;
if (!normalizedSearchTerm) return true;
const haystack = [
purchase.email_address,
purchase.project_name,
purchase.tag_name,
purchase.used ? '已用 used' : '未用 unused',
purchase.preserved ? '保留 preserved' : '',
purchase.disabled ? '已禁用 disabled' : '',
purchase.reusable ? '可复用 reusable' : '',
].join(' ').toLowerCase();
return haystack.includes(normalizedSearchTerm);
});
}
function pruneLuckmailSelection(purchases = renderedPurchases) {
const existingIds = new Set((Array.isArray(purchases) ? purchases : []).map((purchase) => String(purchase.id)));
selectedPurchaseIds = new Set([...selectedPurchaseIds].filter((id) => existingIds.has(id)));
}
function updateLuckmailBulkUI(visiblePurchases = getFilteredLuckmailPurchases()) {
if (!dom.checkboxLuckmailSelectAll || !dom.luckmailSelectionSummary) {
return;
}
const visibleIds = visiblePurchases.map((purchase) => String(purchase.id));
const selectedVisibleCount = visibleIds.filter((id) => selectedPurchaseIds.has(id)).length;
const hasVisible = visibleIds.length > 0;
const hasSelection = selectedPurchaseIds.size > 0;
dom.checkboxLuckmailSelectAll.checked = hasVisible && selectedVisibleCount === visibleIds.length;
dom.checkboxLuckmailSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleIds.length;
dom.checkboxLuckmailSelectAll.disabled = !hasVisible;
dom.luckmailSelectionSummary.textContent = `已选 ${selectedPurchaseIds.size} 个(当前显示 ${visibleIds.length} 个)`;
if (dom.btnLuckmailBulkUsed) dom.btnLuckmailBulkUsed.disabled = !hasSelection;
if (dom.btnLuckmailBulkUnused) dom.btnLuckmailBulkUnused.disabled = !hasSelection;
if (dom.btnLuckmailBulkPreserve) dom.btnLuckmailBulkPreserve.disabled = !hasSelection;
if (dom.btnLuckmailBulkUnpreserve) dom.btnLuckmailBulkUnpreserve.disabled = !hasSelection;
if (dom.btnLuckmailBulkDisable) dom.btnLuckmailBulkDisable.disabled = !hasSelection;
if (dom.btnLuckmailBulkEnable) dom.btnLuckmailBulkEnable.disabled = !hasSelection;
}
function setLuckmailLoadingState(loading, summary = '') {
if (dom.btnLuckmailRefresh) dom.btnLuckmailRefresh.disabled = loading;
if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = loading;
if (dom.inputLuckmailSearch) dom.inputLuckmailSearch.disabled = loading;
if (dom.selectLuckmailFilter) dom.selectLuckmailFilter.disabled = loading;
if (dom.checkboxLuckmailSelectAll) dom.checkboxLuckmailSelectAll.disabled = loading || getFilteredLuckmailPurchases().length === 0;
if (dom.btnLuckmailBulkUsed) dom.btnLuckmailBulkUsed.disabled = loading || selectedPurchaseIds.size === 0;
if (dom.btnLuckmailBulkUnused) dom.btnLuckmailBulkUnused.disabled = loading || selectedPurchaseIds.size === 0;
if (dom.btnLuckmailBulkPreserve) dom.btnLuckmailBulkPreserve.disabled = loading || selectedPurchaseIds.size === 0;
if (dom.btnLuckmailBulkUnpreserve) dom.btnLuckmailBulkUnpreserve.disabled = loading || selectedPurchaseIds.size === 0;
if (dom.btnLuckmailBulkDisable) dom.btnLuckmailBulkDisable.disabled = loading || selectedPurchaseIds.size === 0;
if (dom.btnLuckmailBulkEnable) dom.btnLuckmailBulkEnable.disabled = loading || selectedPurchaseIds.size === 0;
if (summary && dom.luckmailSummary) {
dom.luckmailSummary.textContent = summary;
}
}
function renderLuckmailPurchases(purchases = renderedPurchases) {
if (!dom.luckmailList || !dom.luckmailSummary) return;
renderedPurchases = Array.isArray(purchases) ? purchases : [];
pruneLuckmailSelection(renderedPurchases);
dom.luckmailList.innerHTML = '';
if (!renderedPurchases.length) {
selectedPurchaseIds.clear();
dom.luckmailList.innerHTML = '<div class="luckmail-empty">未找到 openai 项目的 LuckMail 邮箱。</div>';
dom.luckmailSummary.textContent = '加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。';
if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = true;
updateLuckmailBulkUI([]);
return;
}
const usedCount = renderedPurchases.filter((purchase) => purchase.used).length;
const reusableCount = renderedPurchases.filter((purchase) => purchase.reusable).length;
const disableUsedCount = renderedPurchases.filter((purchase) => purchase.used && !purchase.preserved && !purchase.disabled).length;
dom.luckmailSummary.textContent = `已加载 ${renderedPurchases.length} 个 openai 邮箱,其中 ${reusableCount} 个可复用,${usedCount} 个已本地标记为已用。`;
if (dom.btnLuckmailDisableUsed) {
dom.btnLuckmailDisableUsed.textContent = `禁用已用${disableUsedCount > 0 ? `${disableUsedCount}` : ''}`;
dom.btnLuckmailDisableUsed.disabled = disableUsedCount === 0;
}
const visiblePurchases = getFilteredLuckmailPurchases(renderedPurchases);
if (!visiblePurchases.length) {
dom.luckmailList.innerHTML = '<div class="luckmail-empty">没有匹配当前筛选条件的 LuckMail 邮箱。</div>';
updateLuckmailBulkUI([]);
return;
}
for (const purchase of visiblePurchases) {
const purchaseId = String(purchase.id);
const item = document.createElement('div');
item.className = `luckmail-item${purchase.current ? ' is-current' : ''}`;
item.innerHTML = `
<input class="luckmail-item-check" type="checkbox" data-action="select" ${selectedPurchaseIds.has(purchaseId) ? 'checked' : ''} />
<div class="luckmail-item-main">
<div class="luckmail-item-email-row">
<div class="luckmail-item-email">${helpers.escapeHtml(purchase.email_address || '(未知邮箱)')}</div>
<button
class="hotmail-copy-btn"
type="button"
data-action="copy-email"
title="复制邮箱"
aria-label="复制邮箱 ${helpers.escapeHtml(purchase.email_address || '')}"
>${copyIcon}</button>
</div>
<div class="luckmail-item-meta">
<span class="luckmail-tag">${helpers.escapeHtml(helpers.normalizeLuckmailProjectName(purchase.project_name) || 'openai')}</span>
${purchase.reusable ? '<span class="luckmail-tag active">可复用</span>' : ''}
${purchase.current ? '<span class="luckmail-tag current">当前</span>' : ''}
${purchase.used ? '<span class="luckmail-tag used">已用</span>' : ''}
${purchase.preserved ? '<span class="luckmail-tag">保留</span>' : ''}
${purchase.disabled ? '<span class="luckmail-tag disabled">已禁用</span>' : ''}
${purchase.tag_name && normalizeLuckmailSearchText(purchase.tag_name) !== normalizeLuckmailSearchText(helpers.getLuckmailPreserveTagName())
? `<span class="luckmail-tag">${helpers.escapeHtml(purchase.tag_name)}</span>`
: ''}
</div>
<div class="luckmail-item-details">
<span>ID${helpers.escapeHtml(String(purchase.id || ''))}</span>
<span>保修至:${helpers.escapeHtml(helpers.formatLuckmailDateTime(purchase.warranty_until))}</span>
</div>
</div>
<div class="luckmail-item-actions">
<button class="btn btn-outline btn-xs" type="button" data-action="use">使用此邮箱</button>
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-used">${helpers.escapeHtml(purchase.used ? '标记未用' : '标记已用')}</button>
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-preserved">${helpers.escapeHtml(purchase.preserved ? '取消保留' : '保留')}</button>
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-disabled">${helpers.escapeHtml(purchase.disabled ? '启用' : '禁用')}</button>
</div>
`;
item.querySelector('[data-action="select"]').addEventListener('change', (event) => {
if (event.target.checked) {
selectedPurchaseIds.add(purchaseId);
} else {
selectedPurchaseIds.delete(purchaseId);
}
updateLuckmailBulkUI(visiblePurchases);
});
item.querySelector('[data-action="copy-email"]').addEventListener('click', async () => {
await helpers.copyTextToClipboard(purchase.email_address || '');
helpers.showToast('邮箱已复制', 'success', 1600);
});
item.querySelector('[data-action="use"]').addEventListener('click', async () => {
await selectSingleLuckmailPurchase(purchase);
});
item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => {
await setSingleLuckmailPurchaseUsedState(purchase, !purchase.used);
});
item.querySelector('[data-action="toggle-preserved"]').addEventListener('click', async () => {
await setSingleLuckmailPurchasePreservedState(purchase, !purchase.preserved);
});
item.querySelector('[data-action="toggle-disabled"]').addEventListener('click', async () => {
await setSingleLuckmailPurchaseDisabledState(purchase, !purchase.disabled);
});
dom.luckmailList.appendChild(item);
}
updateLuckmailBulkUI(visiblePurchases);
}
async function refreshLuckmailPurchases(options = {}) {
const { silent = false } = options;
if (!dom.luckmailSection || dom.luckmailSection.style.display === 'none') {
return;
}
if (!silent) setLuckmailLoadingState(true, '正在加载 LuckMail openai 邮箱...');
try {
const response = await runtime.sendMessage({
type: 'LIST_LUCKMAIL_PURCHASES',
source: 'sidepanel',
payload: {},
});
if (response?.error) throw new Error(response.error);
renderLuckmailPurchases(response?.purchases || []);
} catch (err) {
selectedPurchaseIds.clear();
if (dom.luckmailList) {
dom.luckmailList.innerHTML = '<div class="luckmail-empty">无法加载 LuckMail 邮箱列表。</div>';
}
if (dom.luckmailSummary) {
dom.luckmailSummary.textContent = err.message;
}
updateLuckmailBulkUI([]);
if (!silent) {
helpers.showToast(`LuckMail 邮箱列表加载失败:${err.message}`, 'error');
}
} finally {
setLuckmailLoadingState(false);
}
}
function queueLuckmailPurchaseRefresh() {
if (refreshQueued) return;
refreshQueued = true;
setTimeout(async () => {
refreshQueued = false;
await refreshLuckmailPurchases({ silent: true });
}, 150);
}
async function selectSingleLuckmailPurchase(purchase) {
setLuckmailLoadingState(true, `正在切换到 ${purchase.email_address} ...`);
try {
const response = await runtime.sendMessage({
type: 'SELECT_LUCKMAIL_PURCHASE',
source: 'sidepanel',
payload: { purchaseId: purchase.id },
});
if (response?.error) throw new Error(response.error);
dom.inputEmail.value = response?.purchase?.email_address || purchase.email_address || '';
helpers.showToast(`已切换当前 LuckMail 邮箱为 ${purchase.email_address}`, 'success', 2200);
await refreshLuckmailPurchases({ silent: true });
} catch (err) {
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
helpers.showToast(`切换 LuckMail 邮箱失败:${err.message}`, 'error');
} finally {
setLuckmailLoadingState(false);
}
}
async function setSingleLuckmailPurchaseUsedState(purchase, used) {
setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的已用状态...`);
try {
const response = await runtime.sendMessage({
type: 'SET_LUCKMAIL_PURCHASE_USED_STATE',
source: 'sidepanel',
payload: { purchaseId: purchase.id, used },
});
if (response?.error) throw new Error(response.error);
helpers.showToast(`${purchase.email_address}${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
await refreshLuckmailPurchases({ silent: true });
} catch (err) {
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
helpers.showToast(`更新 LuckMail 已用状态失败:${err.message}`, 'error');
} finally {
setLuckmailLoadingState(false);
}
}
async function setSingleLuckmailPurchasePreservedState(purchase, preserved) {
setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的保留状态...`);
try {
const response = await runtime.sendMessage({
type: 'SET_LUCKMAIL_PURCHASE_PRESERVED_STATE',
source: 'sidepanel',
payload: { purchaseId: purchase.id, preserved },
});
if (response?.error) throw new Error(response.error);
helpers.showToast(`${purchase.email_address}${preserved ? '设为保留' : '取消保留'}`, 'success', 2200);
await refreshLuckmailPurchases({ silent: true });
} catch (err) {
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
helpers.showToast(`更新 LuckMail 保留状态失败:${err.message}`, 'error');
} finally {
setLuckmailLoadingState(false);
}
}
async function setSingleLuckmailPurchaseDisabledState(purchase, disabled) {
setLuckmailLoadingState(true, `正在${disabled ? '禁用' : '启用'} ${purchase.email_address} ...`);
try {
const response = await runtime.sendMessage({
type: 'SET_LUCKMAIL_PURCHASE_DISABLED_STATE',
source: 'sidepanel',
payload: { purchaseId: purchase.id, disabled },
});
if (response?.error) throw new Error(response.error);
helpers.showToast(`${purchase.email_address}${disabled ? '禁用' : '启用'}`, 'success', 2200);
await refreshLuckmailPurchases({ silent: true });
} catch (err) {
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
helpers.showToast(`更新 LuckMail 禁用状态失败:${err.message}`, 'error');
} finally {
setLuckmailLoadingState(false);
}
}
async function runBulkLuckmailAction(action) {
const selectedIds = renderedPurchases
.filter((purchase) => selectedPurchaseIds.has(String(purchase.id)))
.map((purchase) => purchase.id);
if (!selectedIds.length) {
updateLuckmailBulkUI();
return;
}
const actionLabelMap = {
used: '标记已用',
unused: '标记未用',
preserve: '保留',
unpreserve: '取消保留',
disable: '禁用',
enable: '启用',
};
setLuckmailLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} LuckMail 邮箱...`);
try {
const response = await runtime.sendMessage({
type: 'BATCH_UPDATE_LUCKMAIL_PURCHASES',
source: 'sidepanel',
payload: { action, ids: selectedIds },
});
if (response?.error) throw new Error(response.error);
helpers.showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedIds.length} 个 LuckMail 邮箱`, 'success', 2400);
await refreshLuckmailPurchases({ silent: true });
} catch (err) {
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
helpers.showToast(`批量处理 LuckMail 邮箱失败:${err.message}`, 'error');
} finally {
setLuckmailLoadingState(false);
updateLuckmailBulkUI();
}
}
async function disableUsedLuckmailPurchases() {
const confirmed = await helpers.openConfirmModal({
title: '禁用已用 LuckMail 邮箱',
message: '确认禁用所有本地已用且未保留的 openai LuckMail 邮箱吗?',
confirmLabel: '确认禁用',
confirmVariant: 'btn-danger',
});
if (!confirmed) {
return;
}
setLuckmailLoadingState(true, '正在禁用已用 LuckMail 邮箱...');
try {
const response = await runtime.sendMessage({
type: 'DISABLE_USED_LUCKMAIL_PURCHASES',
source: 'sidepanel',
payload: {},
});
if (response?.error) throw new Error(response.error);
const disabledCount = Array.isArray(response?.disabledIds) ? response.disabledIds.length : 0;
helpers.showToast(`已禁用 ${disabledCount} 个 LuckMail 邮箱`, disabledCount > 0 ? 'success' : 'info', 2400);
await refreshLuckmailPurchases({ silent: true });
} catch (err) {
if (dom.luckmailSummary) dom.luckmailSummary.textContent = err.message;
helpers.showToast(`禁用已用 LuckMail 邮箱失败:${err.message}`, 'error');
} finally {
setLuckmailLoadingState(false);
}
}
function reset() {
renderedPurchases = [];
selectedPurchaseIds.clear();
searchTerm = '';
filterMode = 'all';
refreshQueued = false;
if (dom.inputLuckmailSearch) dom.inputLuckmailSearch.value = '';
if (dom.selectLuckmailFilter) dom.selectLuckmailFilter.value = 'all';
if (dom.luckmailList) dom.luckmailList.innerHTML = '';
if (dom.luckmailSummary) dom.luckmailSummary.textContent = '加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。';
if (dom.btnLuckmailDisableUsed) dom.btnLuckmailDisableUsed.disabled = true;
updateLuckmailBulkUI([]);
}
function bindLuckmailEvents() {
dom.inputLuckmailSearch?.addEventListener('input', (event) => {
searchTerm = event.target.value || '';
renderLuckmailPurchases(renderedPurchases);
});
dom.selectLuckmailFilter?.addEventListener('change', (event) => {
filterMode = String(event.target.value || 'all').trim() || 'all';
renderLuckmailPurchases(renderedPurchases);
});
dom.checkboxLuckmailSelectAll?.addEventListener('change', () => {
const visiblePurchases = getFilteredLuckmailPurchases();
if (dom.checkboxLuckmailSelectAll.checked) {
visiblePurchases.forEach((purchase) => selectedPurchaseIds.add(String(purchase.id)));
} else {
visiblePurchases.forEach((purchase) => selectedPurchaseIds.delete(String(purchase.id)));
}
renderLuckmailPurchases(renderedPurchases);
});
dom.btnLuckmailRefresh?.addEventListener('click', async () => {
await refreshLuckmailPurchases();
});
dom.btnLuckmailDisableUsed?.addEventListener('click', async () => {
await disableUsedLuckmailPurchases();
});
dom.btnLuckmailBulkUsed?.addEventListener('click', async () => {
await runBulkLuckmailAction('used');
});
dom.btnLuckmailBulkUnused?.addEventListener('click', async () => {
await runBulkLuckmailAction('unused');
});
dom.btnLuckmailBulkPreserve?.addEventListener('click', async () => {
await runBulkLuckmailAction('preserve');
});
dom.btnLuckmailBulkUnpreserve?.addEventListener('click', async () => {
await runBulkLuckmailAction('unpreserve');
});
dom.btnLuckmailBulkDisable?.addEventListener('click', async () => {
await runBulkLuckmailAction('disable');
});
dom.btnLuckmailBulkEnable?.addEventListener('click', async () => {
await runBulkLuckmailAction('enable');
});
}
return {
bindLuckmailEvents,
disableUsedLuckmailPurchases,
queueLuckmailPurchaseRefresh,
refreshLuckmailPurchases,
renderLuckmailPurchases,
reset,
};
}
globalScope.SidepanelLuckmailManager = {
createLuckmailManager,
};
})(window);
+596
View File
@@ -0,0 +1,596 @@
(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 = '';
let searchTerm = '';
let filterMode = 'all';
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 normalizeSearchText(value = '') {
return String(value || '').trim().toLowerCase();
}
function getStatusKey(account) {
return typeof mail2925Utils.getMail2925AccountStatus === 'function'
? mail2925Utils.getMail2925AccountStatus(account, Date.now())
: 'ready';
}
function getFilteredMail2925Accounts(accounts, currentId = '') {
const normalizedSearchTerm = normalizeSearchText(searchTerm);
return accounts.filter((account) => {
const statusKey = getStatusKey(account);
const status = getStatusSnapshot(account);
const isCurrent = Boolean(currentId) && account.id === currentId;
const matchesFilter = (() => {
switch (filterMode) {
case 'current': return isCurrent;
case 'ready': return statusKey === 'ready';
case 'cooldown': return statusKey === 'cooldown';
case 'disabled': return statusKey === 'disabled';
case 'error': return statusKey === 'error';
default: return true;
}
})();
if (!matchesFilter) return false;
if (!normalizedSearchTerm) return true;
const haystack = [
account.email,
statusKey,
status.label,
isCurrent ? 'current 当前' : '',
].join(' ').toLowerCase();
return haystack.includes(normalizedSearchTerm);
});
}
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;
}
const visibleAccounts = getFilteredMail2925Accounts(accounts, currentId);
if (!visibleAccounts.length) {
dom.mail2925AccountsList.innerHTML = '<div class="hotmail-empty">没有匹配当前筛选条件的 2925 账号。</div>';
updateMail2925ListViewport();
return;
}
dom.mail2925AccountsList.innerHTML = visibleAccounts.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.inputMail2925Search?.addEventListener('input', (event) => {
searchTerm = normalizeSearchText(event.target.value);
renderMail2925Accounts();
});
dom.selectMail2925Filter?.addEventListener('change', (event) => {
filterMode = String(event.target.value || 'all');
renderMail2925Accounts();
});
dom.mail2925AccountsList?.addEventListener('click', handleAccountListClick);
syncEditUi();
formController.sync();
}
return {
bindMail2925Events,
initMail2925ListExpandedState,
renderMail2925Accounts,
};
}
globalScope.SidepanelMail2925Manager = {
createMail2925Manager,
};
})(window);
+308
View File
@@ -0,0 +1,308 @@
(function attachSidepanelPayPalManager(globalScope) {
function createPayPalManager(context = {}) {
const {
state,
dom,
helpers,
runtime,
paypalUtils = {},
} = context;
let actionInFlight = false;
let payPalAccountPicker = null;
function getPayPalAccounts(currentState = state.getLatestState()) {
return helpers.getPayPalAccounts(currentState);
}
function getCurrentPayPalAccountId(currentState = state.getLatestState()) {
return String(currentState?.currentPayPalAccountId || '').trim();
}
function buildSelectOptions(accounts = []) {
if (!accounts.length) {
return '<option value=""></option>';
}
return accounts.map((account) => (
`<option value="${helpers.escapeHtml(account.id)}">${helpers.escapeHtml(account.email || '(未命名账号)')}</option>`
)).join('');
}
function getPayPalAccountValue(account = {}) {
return String(account?.id || '').trim();
}
function getPayPalAccountLabel(account = {}) {
return String(account?.email || '(未命名账号)');
}
function normalizePickerPayPalAccounts(accounts = []) {
return Array.isArray(accounts)
? accounts.filter((account) => getPayPalAccountValue(account))
: [];
}
function getPayPalAccountPicker() {
if (payPalAccountPicker) {
return payPalAccountPicker;
}
const pickerModule = helpers.editableListPicker || globalScope.SidepanelEditableListPicker;
const createEditableListPicker = pickerModule?.createEditableListPicker;
if (
typeof createEditableListPicker !== 'function'
|| !dom.payPalAccountPickerRoot
|| !dom.btnPayPalAccountMenu
|| !dom.payPalAccountCurrent
|| !dom.payPalAccountMenu
) {
return null;
}
payPalAccountPicker = createEditableListPicker({
root: dom.payPalAccountPickerRoot,
input: dom.selectPayPalAccount,
trigger: dom.btnPayPalAccountMenu,
current: dom.payPalAccountCurrent,
menu: dom.payPalAccountMenu,
emptyLabel: '',
itemLabel: '账号',
normalizeItems: normalizePickerPayPalAccounts,
normalizeValue: (value) => String(value || '').trim(),
getItemValue: getPayPalAccountValue,
getItemLabel: getPayPalAccountLabel,
getItemDeleteLabel: getPayPalAccountLabel,
onDelete: (accountId) => handleDeletePayPalAccount(accountId),
onDeleteError: (error, fallbackMessage) => {
helpers.showToast(error?.message || fallbackMessage, 'error');
},
});
return payPalAccountPicker;
}
function applyPayPalAccountMutation(account) {
if (!account?.id) return;
const latestState = state.getLatestState();
const nextAccounts = typeof paypalUtils.upsertPayPalAccountInList === 'function'
? paypalUtils.upsertPayPalAccountInList(getPayPalAccounts(latestState), account)
: [...getPayPalAccounts(latestState), account];
state.syncLatestState({ paypalAccounts: nextAccounts });
renderPayPalAccounts();
}
function renderPayPalAccounts() {
if (!dom.selectPayPalAccount) return;
const latestState = state.getLatestState();
const accounts = getPayPalAccounts(latestState);
const currentId = getCurrentPayPalAccountId(latestState);
const selectedId = accounts.some((account) => account.id === currentId) ? currentId : '';
const picker = getPayPalAccountPicker();
if (picker) {
picker.render(accounts, selectedId);
return;
}
dom.selectPayPalAccount.innerHTML = buildSelectOptions(accounts);
dom.selectPayPalAccount.disabled = accounts.length === 0;
dom.selectPayPalAccount.value = selectedId;
}
async function syncSelectedPayPalAccount(options = {}) {
const { silent = false } = options;
const accountId = String(dom.selectPayPalAccount?.value || '').trim();
if (!accountId) {
state.syncLatestState({
currentPayPalAccountId: null,
paypalEmail: '',
paypalPassword: '',
});
renderPayPalAccounts();
return null;
}
const response = await runtime.sendMessage({
type: 'SELECT_PAYPAL_ACCOUNT',
source: 'sidepanel',
payload: { accountId },
});
if (response?.error) {
throw new Error(response.error);
}
state.syncLatestState({
currentPayPalAccountId: response.account?.id || accountId,
paypalEmail: String(response.account?.email || '').trim(),
paypalPassword: String(response.account?.password || ''),
});
renderPayPalAccounts();
if (!silent) {
helpers.showToast(`已切换当前 PayPal 账号为 ${response.account?.email || accountId}`, 'success', 1800);
}
return response.account || null;
}
async function handleDeletePayPalAccount(accountId) {
if (actionInFlight) return;
const targetId = String(accountId || '').trim();
if (!targetId) {
return;
}
const latestState = state.getLatestState();
const accounts = getPayPalAccounts(latestState);
const targetAccount = accounts.find((account) => account.id === targetId);
if (!targetAccount) {
return;
}
actionInFlight = true;
if (dom.btnAddPayPalAccount) {
dom.btnAddPayPalAccount.disabled = true;
}
try {
const nextAccounts = accounts.filter((account) => account.id !== targetId);
const currentId = getCurrentPayPalAccountId(latestState);
const nextCurrentAccount = currentId === targetId
? (nextAccounts[0] || null)
: (nextAccounts.find((account) => account.id === currentId) || null);
const payload = {
paypalAccounts: nextAccounts,
currentPayPalAccountId: nextCurrentAccount?.id || '',
paypalEmail: String(nextCurrentAccount?.email || '').trim(),
paypalPassword: String(nextCurrentAccount?.password || ''),
};
const response = await runtime.sendMessage({
type: 'SAVE_SETTING',
source: 'sidepanel',
payload,
});
if (response?.error) {
throw new Error(response.error);
}
state.syncLatestState({
...payload,
currentPayPalAccountId: payload.currentPayPalAccountId || null,
});
renderPayPalAccounts();
helpers.showToast(`已删除 PayPal 账号:${targetAccount.email || targetId}`, 'success', 1600);
} finally {
actionInFlight = false;
if (dom.btnAddPayPalAccount) {
dom.btnAddPayPalAccount.disabled = false;
}
}
}
async function openPayPalAccountDialog() {
if (typeof helpers.openFormDialog !== 'function') {
throw new Error('表单弹窗能力未加载,请刷新扩展后重试。');
}
return helpers.openFormDialog({
title: '添加 PayPal 账号',
confirmLabel: '保存账号',
confirmVariant: 'btn-primary',
fields: [
{
key: 'email',
label: 'PayPal 账号',
type: 'text',
placeholder: '请输入 PayPal 登录邮箱',
autocomplete: 'username',
required: true,
requiredMessage: '请先填写 PayPal 账号。',
validate: (value) => {
const normalized = String(value || '').trim();
if (!normalized.includes('@')) {
return 'PayPal 账号需填写邮箱格式。';
}
return '';
},
},
{
key: 'password',
label: 'PayPal 密码',
type: 'password',
placeholder: '请输入 PayPal 登录密码',
autocomplete: 'current-password',
required: true,
requiredMessage: '请先填写 PayPal 密码。',
},
],
});
}
async function handleAddPayPalAccount() {
if (actionInFlight) return;
const formValues = await openPayPalAccountDialog();
if (!formValues) {
return;
}
actionInFlight = true;
if (dom.btnAddPayPalAccount) {
dom.btnAddPayPalAccount.disabled = true;
}
try {
const response = await runtime.sendMessage({
type: 'UPSERT_PAYPAL_ACCOUNT',
source: 'sidepanel',
payload: {
email: String(formValues.email || '').trim(),
password: String(formValues.password || ''),
},
});
if (response?.error) {
throw new Error(response.error);
}
applyPayPalAccountMutation(response.account);
if (response.account?.id) {
state.syncLatestState({ currentPayPalAccountId: response.account.id });
renderPayPalAccounts();
dom.selectPayPalAccount.value = response.account.id;
await syncSelectedPayPalAccount({ silent: true });
}
helpers.showToast(`已保存 PayPal 账号 ${response.account?.email || ''}`, 'success', 2200);
} catch (error) {
helpers.showToast(`保存 PayPal 账号失败:${error.message}`, 'error');
throw error;
} finally {
actionInFlight = false;
if (dom.btnAddPayPalAccount) {
dom.btnAddPayPalAccount.disabled = false;
}
}
}
function bindPayPalEvents() {
dom.btnAddPayPalAccount?.addEventListener('click', () => {
void handleAddPayPalAccount();
});
dom.selectPayPalAccount?.addEventListener('change', () => {
void syncSelectedPayPalAccount().catch((error) => {
helpers.showToast(error.message, 'error');
renderPayPalAccounts();
});
});
}
return {
bindPayPalEvents,
handleDeletePayPalAccount,
renderPayPalAccounts,
syncSelectedPayPalAccount,
};
}
globalScope.SidepanelPayPalManager = {
createPayPalManager,
};
})(window);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16419
View File
File diff suppressed because it is too large Load Diff
+459
View File
@@ -0,0 +1,459 @@
(() => {
const GITHUB_OWNER = 'FoundZiGu';
const GITHUB_REPO = 'GuJumpgate';
const RELEASES_PAGE_URL = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases`;
const RELEASES_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases?per_page=10`;
const CACHE_KEY = 'multipage-release-snapshot-v1';
const IGNORED_UPDATE_VERSION_KEY = 'multipage-ignored-release-version-v1';
const CACHE_TTL_MS = 60 * 60 * 1000;
const FETCH_TIMEOUT_MS = 8000;
const MAX_RELEASES = 10;
const MAX_NOTES_PER_RELEASE = 5;
const VERSION_FAMILY_GUJUMPGATE = 'gujumpgate';
const VERSION_FAMILY_ULTRA = 'ultra';
const VERSION_FAMILY_PRO = 'pro';
const VERSION_FAMILY_LEGACY = 'legacy';
function getVersionFamily(version, fallbackFamily = VERSION_FAMILY_LEGACY) {
const trimmed = String(version || '').trim();
if (/^(?:gujumpgate|flowpilot)/i.test(trimmed)) {
return VERSION_FAMILY_GUJUMPGATE;
}
if (/^ultra/i.test(trimmed)) {
return VERSION_FAMILY_ULTRA;
}
if (/^pro/i.test(trimmed)) {
return VERSION_FAMILY_PRO;
}
if (/^v/i.test(trimmed)) {
return VERSION_FAMILY_LEGACY;
}
return fallbackFamily;
}
function stripVersionPrefix(version) {
return String(version || '').trim().replace(/^(?:gujumpgate|flowpilot|ultra|pro|v)\s*/i, '');
}
function extractVersionCore(version) {
const core = stripVersionPrefix(version).split('-')[0];
return /^\d+(?:\.\d+){1,3}$/.test(core) ? core : '';
}
function formatDisplayVersion(version, fallbackFamily = VERSION_FAMILY_LEGACY) {
const core = extractVersionCore(version);
if (!core) {
return '';
}
const family = getVersionFamily(version, fallbackFamily);
return `${getVersionFamilyPrefix(family)}${core}`;
}
function parseVersionMeta(version, fallbackFamily = VERSION_FAMILY_LEGACY) {
const family = getVersionFamily(version, fallbackFamily);
const core = extractVersionCore(version);
return {
family,
core,
displayVersion: core ? `${getVersionFamilyPrefix(family)}${core}` : '',
parts: core
? core.split('.').map((part) => {
const numeric = Number.parseInt(part, 10);
return Number.isFinite(numeric) ? numeric : 0;
})
: [0],
};
}
function getVersionFamilyPrefix(family) {
if (family === VERSION_FAMILY_GUJUMPGATE) {
return 'GuJumpgate';
}
if (family === VERSION_FAMILY_ULTRA) {
return 'Ultra';
}
if (family === VERSION_FAMILY_PRO) {
return 'Pro';
}
return 'v';
}
function getVersionFamilyRank(family) {
if (family === VERSION_FAMILY_GUJUMPGATE) {
return 4;
}
if (family === VERSION_FAMILY_ULTRA) {
return 3;
}
if (family === VERSION_FAMILY_PRO) {
return 2;
}
return 1;
}
function parseVersionParts(version, fallbackFamily = VERSION_FAMILY_LEGACY) {
return parseVersionMeta(version, fallbackFamily).parts;
}
function compareVersions(left, right, fallbackFamily = VERSION_FAMILY_LEGACY) {
const leftMeta = parseVersionMeta(left, fallbackFamily);
const rightMeta = parseVersionMeta(right, fallbackFamily);
const leftFamilyRank = getVersionFamilyRank(leftMeta.family);
const rightFamilyRank = getVersionFamilyRank(rightMeta.family);
if (leftFamilyRank > rightFamilyRank) {
return 1;
}
if (leftFamilyRank < rightFamilyRank) {
return -1;
}
const leftParts = leftMeta.parts;
const rightParts = rightMeta.parts;
const maxLength = Math.max(leftParts.length, rightParts.length, 3);
for (let index = 0; index < maxLength; index += 1) {
const leftPart = leftParts[index] || 0;
const rightPart = rightParts[index] || 0;
if (leftPart > rightPart) {
return 1;
}
if (leftPart < rightPart) {
return -1;
}
}
return 0;
}
function sanitizeInlineMarkdown(text) {
return String(text || '')
.replace(/!\[[^\]]*]\(([^)]+)\)/g, '')
.replace(/\[([^\]]+)]\(([^)]+)\)/g, '$1')
.replace(/`([^`]+)`/g, '$1')
.replace(/[*_~>#]/g, '')
.replace(/\s+/g, ' ')
.trim();
}
function parseReleaseNotes(body) {
const lines = String(body || '')
.replace(/\r\n/g, '\n')
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
const bulletLines = [];
const plainLines = [];
for (const line of lines) {
if (/^#{1,6}\s*/.test(line)) {
continue;
}
if (/^```/.test(line) || /^---+$/.test(line)) {
continue;
}
if (/^[-*+]\s+/.test(line)) {
bulletLines.push(line.replace(/^[-*+]\s+/, ''));
continue;
}
if (/^\d+\.\s+/.test(line)) {
bulletLines.push(line.replace(/^\d+\.\s+/, ''));
continue;
}
plainLines.push(line);
}
const noteLines = bulletLines.length > 0 ? bulletLines : plainLines;
return noteLines
.map(sanitizeInlineMarkdown)
.filter(Boolean)
.slice(0, MAX_NOTES_PER_RELEASE);
}
function normalizeReleaseVersion(release) {
const candidates = [
release?.tag_name,
release?.name,
];
for (const candidate of candidates) {
const meta = parseVersionMeta(candidate);
if (meta.core) {
return meta;
}
}
return null;
}
function sanitizeRelease(release) {
const versionMeta = normalizeReleaseVersion(release);
if (!versionMeta?.core) {
return null;
}
const rawTitle = sanitizeInlineMarkdown(release?.name || '');
const titleMeta = parseVersionMeta(rawTitle, versionMeta.family);
const normalizedTitle = titleMeta.core && titleMeta.displayVersion === versionMeta.displayVersion
? ''
: rawTitle;
return {
version: versionMeta.core,
displayVersion: versionMeta.displayVersion,
family: versionMeta.family,
title: normalizedTitle,
url: String(release?.html_url || RELEASES_PAGE_URL),
publishedAt: String(release?.published_at || release?.created_at || ''),
notes: parseReleaseNotes(release?.body || ''),
};
}
function getComparableReleaseVersion(release) {
const fallbackFamily = [VERSION_FAMILY_GUJUMPGATE, VERSION_FAMILY_ULTRA, VERSION_FAMILY_PRO].includes(release?.family)
? release.family
: VERSION_FAMILY_LEGACY;
const displayVersion = String(release?.displayVersion || '').trim();
if (displayVersion) {
return displayVersion;
}
return formatDisplayVersion(release?.version || '', fallbackFamily);
}
function sortReleasesByVersion(releases = []) {
return [...releases].sort((left, right) => (
compareVersions(
getComparableReleaseVersion(right),
getComparableReleaseVersion(left)
)
));
}
function readCache() {
try {
const raw = localStorage.getItem(CACHE_KEY);
if (!raw) {
return null;
}
const parsed = JSON.parse(raw);
if (!parsed || !Array.isArray(parsed.releases) || !Number.isFinite(parsed.fetchedAt)) {
return null;
}
if ((Date.now() - parsed.fetchedAt) > CACHE_TTL_MS) {
return null;
}
return sortReleasesByVersion(parsed.releases).slice(0, MAX_RELEASES);
} catch (error) {
return null;
}
}
function writeCache(releases) {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify({
fetchedAt: Date.now(),
releases,
}));
} catch (error) {
// Ignore cache write failures.
}
}
function getIgnoredUpdateVersion() {
try {
return String(localStorage.getItem(IGNORED_UPDATE_VERSION_KEY) || '').trim();
} catch (error) {
return '';
}
}
function setIgnoredUpdateVersion(version) {
const normalized = formatDisplayVersion(version, VERSION_FAMILY_GUJUMPGATE) || String(version || '').trim();
try {
if (normalized) {
localStorage.setItem(IGNORED_UPDATE_VERSION_KEY, normalized);
} else {
localStorage.removeItem(IGNORED_UPDATE_VERSION_KEY);
}
} catch (error) {
// Ignore localStorage failures.
}
return normalized;
}
function clearIgnoredUpdateVersion() {
return setIgnoredUpdateVersion('');
}
async function fetchReleases() {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const response = await fetch(RELEASES_API_URL, {
method: 'GET',
headers: {
Accept: 'application/vnd.github+json',
},
cache: 'no-store',
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`GitHub Releases 请求失败(${response.status}`);
}
const payload = await response.json();
if (!Array.isArray(payload)) {
throw new Error('GitHub Releases 返回格式异常');
}
const releases = payload
.filter((release) => release && !release.draft && !release.prerelease)
.map(sanitizeRelease)
.filter(Boolean);
const sortedReleases = sortReleasesByVersion(releases).slice(0, MAX_RELEASES);
writeCache(sortedReleases);
return sortedReleases;
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error('GitHub Releases 请求超时');
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
async function loadReleases(options = {}) {
if (!options.force) {
const cached = readCache();
if (cached) {
return cached;
}
}
return fetchReleases();
}
function buildReleaseSnapshot(releases, localVersion) {
const latestRelease = releases[0] || null;
if (!latestRelease) {
return {
status: 'empty',
localVersion,
latestVersion: null,
latestRelease: null,
newerReleases: [],
logUrl: RELEASES_PAGE_URL,
releasesPageUrl: RELEASES_PAGE_URL,
checkedAt: Date.now(),
};
}
const newerReleases = releases.filter((release) => compareVersions(release.displayVersion, localVersion) > 0);
const ignoredVersion = getIgnoredUpdateVersion();
const newestUpdateVersion = newerReleases[0]?.displayVersion || '';
const updateIgnored = Boolean(newestUpdateVersion)
&& Boolean(ignoredVersion)
&& compareVersions(ignoredVersion, newestUpdateVersion) === 0;
return {
status: newerReleases.length > 0
? (updateIgnored ? 'ignored' : 'update-available')
: 'latest',
localVersion,
latestVersion: latestRelease.displayVersion,
latestRelease,
newerReleases,
ignoredVersion: updateIgnored ? ignoredVersion : '',
logUrl: latestRelease.url || RELEASES_PAGE_URL,
releasesPageUrl: RELEASES_PAGE_URL,
checkedAt: Date.now(),
};
}
function getSnapshotUpdateVersion(snapshot = {}) {
const newerReleases = Array.isArray(snapshot?.newerReleases) ? snapshot.newerReleases : [];
return newerReleases[0]?.displayVersion || snapshot?.latestVersion || '';
}
function ignoreReleaseSnapshot(snapshot = {}) {
const targetVersion = getSnapshotUpdateVersion(snapshot);
if (!targetVersion) {
return '';
}
return setIgnoredUpdateVersion(targetVersion);
}
function getLocalVersionLabel(manifest = chrome.runtime.getManifest()) {
const versionName = formatDisplayVersion(manifest?.version_name, VERSION_FAMILY_GUJUMPGATE);
if (versionName) {
return versionName;
}
const versionCore = extractVersionCore(manifest?.version || '');
return versionCore ? formatDisplayVersion(`GuJumpgate${versionCore}`, VERSION_FAMILY_GUJUMPGATE) : '';
}
async function getReleaseSnapshot(options = {}) {
const localVersion = getLocalVersionLabel(chrome.runtime.getManifest()) || 'GuJumpgate0.0';
try {
const releases = await loadReleases(options);
return buildReleaseSnapshot(releases, localVersion);
} catch (error) {
return {
status: 'error',
localVersion,
latestVersion: null,
latestRelease: null,
newerReleases: [],
logUrl: RELEASES_PAGE_URL,
releasesPageUrl: RELEASES_PAGE_URL,
checkedAt: Date.now(),
errorMessage: error?.message || '更新检查失败',
};
}
}
function formatReleaseDate(value) {
if (!value) {
return '';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '';
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
window.SidepanelUpdateService = {
compareVersions,
formatDisplayVersion,
formatReleaseDate,
clearIgnoredUpdateVersion,
getIgnoredUpdateVersion,
getLocalVersionLabel,
getReleaseSnapshot,
ignoreReleaseSnapshot,
repositoryUrl: `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}`,
releasesPageUrl: RELEASES_PAGE_URL,
stripVersionPrefix,
};
})();