重构更新账号日志功能,添加开发者模式

This commit is contained in:
QLHazyCoder
2026-04-17 18:29:53 +08:00
parent 3b28845d86
commit ccea024f9a
16 changed files with 1245 additions and 458 deletions
+2 -1
View File
@@ -4,4 +4,5 @@
/_metadata/
.vscode
/data/account-run-history.txt
/data/account-run-history.txt
/data/account-run-history.json
+3 -1
View File
@@ -61,6 +61,8 @@
- 页面要求填写 `age`
- 支持 `Auto` 多轮运行
- 支持中途 `Stop`
- 支持通过日志区的 `记录` 按钮查看邮箱记录面板,按邮箱展示最终状态、时间、失败标签和重试次数
- 支持将邮箱记录完整快照同步到本地 helper,便于开发者直接查看 `data/account-run-history.json`
- Step 8 会自动寻找 OAuth 同意页的“继续”按钮,并通过 Chrome debugger 输入事件发起点击,然后监听本地回调地址
@@ -229,7 +231,7 @@ python3 scripts/hotmail_helper.py
Hotmail helper listening on http://127.0.0.1:17373
```
看到这行再回到扩展里点 `校验``复制最新验证码`
同时还会输出本地邮箱记录快照文件路径。看到这些输出后,再回到扩展里点 `校验``复制最新验证码`,或开启邮箱记录本地同步
#### 最小排错说明
+12 -1
View File
@@ -722,7 +722,7 @@ function normalizeAccountRunHistoryHelperBaseUrl(rawValue = '') {
try {
const parsed = new URL(value);
if (parsed.pathname === '/append-account-log') {
if (parsed.pathname === '/append-account-log' || parsed.pathname === '/sync-account-run-records') {
parsed.pathname = '';
parsed.search = '';
parsed.hash = '';
@@ -5055,6 +5055,16 @@ async function appendAndBroadcastAccountRunRecord(status, stateOverride = null,
return record;
}
async function clearAndBroadcastAccountRunHistory(stateOverride = null) {
if (!accountRunHistoryHelpers?.clearAccountRunHistory) {
return { clearedCount: 0 };
}
const result = await accountRunHistoryHelpers.clearAccountRunHistory(stateOverride);
await broadcastAccountRunHistoryUpdate();
return result;
}
const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({
addLog,
appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args),
@@ -5757,6 +5767,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
broadcastDataUpdate,
cancelScheduledAutoRun,
checkIcloudSession,
clearAccountRunHistory: (...args) => clearAndBroadcastAccountRunHistory(...args),
clearAutoRunTimerAlarm,
clearLuckmailRuntimeState,
clearStopRequest,
+279 -43
View File
@@ -12,21 +12,176 @@
normalizeAccountRunHistoryHelperBaseUrl,
} = deps;
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 normalizeFinalStatus(status = '') {
const normalized = String(status || '').trim().toLowerCase();
if (!normalized) {
return '';
}
if (normalized === 'success') {
return 'success';
}
if (normalized === 'failed' || /_failed$/.test(normalized)) {
return 'failed';
}
if (normalized === 'stopped' || /_stopped$/.test(normalized)) {
return 'stopped';
}
return '';
}
function extractFailedStep(status = '', detail = '') {
const normalizedStatus = String(status || '').trim().toLowerCase();
const statusMatch = normalizedStatus.match(/^step(\d+)_failed$/);
if (statusMatch) {
const step = Number(statusMatch[1]);
return Number.isInteger(step) && step > 0 ? step : null;
}
const text = String(detail || '').trim();
const detailMatch = text.match(/(?:Step\s+(\d+)|步骤\s*(\d+))/i);
if (!detailMatch) {
return null;
}
const step = Number(detailMatch[1] || detailMatch[2]);
return Number.isInteger(step) && step > 0 ? step : null;
}
function isPhoneVerificationFailure(detail = '') {
const text = String(detail || '').trim();
if (!text) {
return false;
}
return /add[_\s-]?phone/i.test(text)
|| /手机号(?:验证|页面|页)|手机(?:号)?页面|出现手机号验证/.test(text)
|| /进入了手机号页面/.test(text);
}
function buildFailureLabel(finalStatus, failedStep, failureDetail = '') {
if (finalStatus === 'success') {
return '流程完成';
}
if (finalStatus !== 'failed') {
return '';
}
if (isPhoneVerificationFailure(failureDetail)) {
return '出现手机号验证';
}
if (Number.isInteger(failedStep) && failedStep > 0) {
return `步骤 ${failedStep} 失败`;
}
return '流程失败';
}
function buildRecordId(email = '') {
return String(email || '').trim().toLowerCase();
}
function normalizeSource(value = '') {
return String(value || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual';
}
function normalizeAutoRunContext(context) {
if (!context || typeof context !== 'object') {
return null;
}
const currentRun = Math.max(0, Math.floor(Number(context.currentRun) || 0));
const totalRuns = Math.max(0, Math.floor(Number(context.totalRuns) || 0));
const attemptRun = Math.max(0, Math.floor(Number(context.attemptRun) || 0));
if (!currentRun && !totalRuns && !attemptRun) {
return null;
}
return {
currentRun,
totalRuns,
attemptRun,
};
}
function buildAutoRunContextFromState(state = {}) {
return normalizeAutoRunContext({
currentRun: state.autoRunCurrentRun,
totalRuns: state.autoRunTotalRuns,
attemptRun: state.autoRunAttemptRun,
});
}
function getRetryCountFromState(state = {}) {
if (!Boolean(state.autoRunning)) {
return 0;
}
const attemptRun = Math.max(0, Math.floor(Number(state.autoRunAttemptRun) || 0));
return attemptRun > 1 ? attemptRun - 1 : 0;
}
function normalizeAccountRunHistoryRecord(record) {
if (!record || typeof record !== 'object') {
return null;
}
const email = String(record.email || '').trim();
const password = String(record.password || '').trim();
const finalStatus = normalizeFinalStatus(record.finalStatus || record.status || '');
if (!email || !password || !finalStatus || finalStatus === 'stopped') {
return null;
}
const finishedAt = String(record.finishedAt || record.recordedAt || '').trim();
const failureDetail = finalStatus === 'failed'
? String(record.failureDetail || record.reason || '').trim()
: '';
const failedStepCandidate = Number(record.failedStep);
const failedStep = Number.isInteger(failedStepCandidate) && failedStepCandidate > 0
? failedStepCandidate
: extractFailedStep(record.finalStatus || record.status || '', failureDetail);
const autoRunContext = normalizeAutoRunContext(record.autoRunContext);
const retryCount = normalizeRetryCount(
record.retryCount !== undefined
? record.retryCount
: ((autoRunContext?.attemptRun || 0) > 1 ? autoRunContext.attemptRun - 1 : 0)
);
const source = normalizeSource(record.source || (autoRunContext ? 'auto' : 'manual'));
return {
recordId: String(record.recordId || '').trim() || buildRecordId(email),
email,
password,
finalStatus,
finishedAt,
retryCount,
failureLabel: String(record.failureLabel || '').trim() || buildFailureLabel(finalStatus, failedStep, failureDetail),
failureDetail,
failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null,
source,
autoRunContext: source === 'auto' ? autoRunContext : null,
};
}
function normalizeAccountRunHistory(records) {
if (!Array.isArray(records)) {
return [];
}
return records
.filter((item) => item && typeof item === 'object')
.map((item) => ({
email: String(item.email || '').trim(),
password: String(item.password || '').trim(),
status: String(item.status || '').trim().toLowerCase(),
recordedAt: String(item.recordedAt || '').trim(),
reason: String(item.reason || '').trim(),
}))
.filter((item) => item.email && item.password && item.status);
.map((item) => normalizeAccountRunHistoryRecord(item))
.filter(Boolean)
.sort((left, right) => normalizeTimestamp(right.finishedAt) - normalizeTimestamp(left.finishedAt));
}
async function getPersistedAccountRunHistory() {
@@ -39,25 +194,63 @@
}
}
async function setPersistedAccountRunHistory(records) {
const normalizedHistory = normalizeAccountRunHistory(records);
await chrome.storage.local.set({
[ACCOUNT_RUN_HISTORY_STORAGE_KEY]: normalizedHistory,
});
return normalizedHistory;
}
function buildAccountRunHistoryRecord(state = {}, status = '', reason = '') {
const email = String(state.email || '').trim();
const password = String(state.password || state.customPassword || '').trim();
const normalizedStatus = String(status || '').trim().toLowerCase();
const normalizedReason = String(reason || '').trim();
const finalStatus = normalizeFinalStatus(status);
if (!email || !password || !normalizedStatus) {
if (!email || !password || !finalStatus || finalStatus === 'stopped') {
return null;
}
const failureDetail = finalStatus === 'failed' ? String(reason || '').trim() : '';
const failedStep = finalStatus === 'failed' ? extractFailedStep(status, failureDetail) : null;
const source = Boolean(state.autoRunning) ? 'auto' : 'manual';
const autoRunContext = source === 'auto' ? buildAutoRunContextFromState(state) : null;
const retryCount = source === 'auto' ? getRetryCountFromState(state) : 0;
const finishedAt = new Date().toISOString();
return {
recordId: buildRecordId(email),
email,
password,
status: normalizedStatus,
recordedAt: new Date().toISOString(),
reason: normalizedReason,
finalStatus,
finishedAt,
retryCount,
failureLabel: buildFailureLabel(finalStatus, failedStep, failureDetail),
failureDetail,
failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null,
source,
autoRunContext,
};
}
function upsertAccountRunHistoryRecord(history, record) {
const normalizedHistory = normalizeAccountRunHistory(history);
if (!record) {
return normalizedHistory;
}
const recordId = String(record.recordId || '').trim();
const emailKey = String(record.email || '').trim().toLowerCase();
const nextHistory = normalizedHistory.filter((item) => {
const itemRecordId = String(item.recordId || '').trim();
const itemEmailKey = String(item.email || '').trim().toLowerCase();
return itemRecordId !== recordId && itemEmailKey !== emailKey;
});
nextHistory.unshift(record);
return normalizeAccountRunHistory(nextHistory);
}
async function appendAccountRunHistoryRecord(status, stateOverride = null, reason = '') {
const state = stateOverride || await getState();
const record = buildAccountRunHistoryRecord(state, status, reason);
@@ -66,14 +259,39 @@
}
const history = await getPersistedAccountRunHistory();
history.push(record);
await chrome.storage.local.set({
[ACCOUNT_RUN_HISTORY_STORAGE_KEY]: history,
});
const nextHistory = upsertAccountRunHistoryRecord(history, record);
await setPersistedAccountRunHistory(nextHistory);
return record;
}
function shouldAppendAccountRunTextFile(state = {}) {
function summarizeAccountRunHistory(records = []) {
return normalizeAccountRunHistory(records).reduce((summary, record) => {
summary.total += 1;
if (record.finalStatus === 'success') {
summary.success += 1;
} else if (record.finalStatus === 'failed') {
summary.failed += 1;
}
summary.retryTotal += normalizeRetryCount(record.retryCount);
return summary;
}, {
total: 0,
success: 0,
failed: 0,
retryTotal: 0,
});
}
function buildAccountRunHistorySnapshotPayload(records = []) {
const normalizedHistory = normalizeAccountRunHistory(records);
return {
generatedAt: new Date().toISOString(),
summary: summarizeAccountRunHistory(normalizedHistory),
records: normalizedHistory,
};
}
function shouldSyncAccountRunHistorySnapshot(state = {}) {
if (!Boolean(state.accountRunHistoryTextEnabled)) {
return false;
}
@@ -82,49 +300,40 @@
return Boolean(helperBaseUrl);
}
async function appendAccountRunHistoryTextFile(record, stateOverride = null) {
const normalizedRecord = record && typeof record === 'object'
? record
: buildAccountRunHistoryRecord(stateOverride || await getState(), '');
if (!normalizedRecord?.email || !normalizedRecord?.password || !normalizedRecord?.status) {
return null;
}
function shouldAppendAccountRunTextFile(state = {}) {
return shouldSyncAccountRunHistorySnapshot(state);
}
async function syncAccountRunHistorySnapshot(records, stateOverride = null) {
const state = stateOverride || await getState();
if (!shouldAppendAccountRunTextFile(state)) {
return null;
if (!shouldSyncAccountRunHistorySnapshot(state)) {
return '';
}
const helperBaseUrl = normalizeAccountRunHistoryHelperBaseUrl(state.accountRunHistoryHelperBaseUrl);
let response;
try {
response = await fetch(buildLocalHelperEndpoint(helperBaseUrl, '/append-account-log'), {
response = await fetch(buildLocalHelperEndpoint(helperBaseUrl, '/sync-account-run-records'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
email: normalizedRecord.email,
password: normalizedRecord.password,
status: normalizedRecord.status,
recordedAt: normalizedRecord.recordedAt,
reason: normalizedRecord.reason || '',
}),
body: JSON.stringify(buildAccountRunHistorySnapshotPayload(records)),
});
} catch (err) {
throw new Error(`账号文本记录写入失败:无法连接本地 helper${getErrorMessage(err)}`);
throw new Error(`账号记录快照同步失败:无法连接本地 helper${getErrorMessage(err)}`);
}
let payload = null;
try {
payload = await response.json();
} catch (err) {
throw new Error(`账号文本记录写入失败:本地 helper 返回了无法解析的响应(${getErrorMessage(err)}`);
throw new Error(`账号记录快照同步失败:本地 helper 返回了无法解析的响应(${getErrorMessage(err)}`);
}
if (!response.ok || payload?.ok === false) {
throw new Error(`账号文本记录写入失败:${payload?.error || `HTTP ${response.status}`}`);
throw new Error(`账号记录快照同步失败:${payload?.error || `HTTP ${response.status}`}`);
}
return payload?.filePath || '';
@@ -138,9 +347,10 @@
}
try {
const filePath = await appendAccountRunHistoryTextFile(record, state);
const history = await getPersistedAccountRunHistory();
const filePath = await syncAccountRunHistorySnapshot(history, state);
if (filePath) {
await addLog(`账号记录已追加到本地文本${filePath}`, 'info');
await addLog(`账号记录快照已同步到本地:${filePath}`, 'info');
}
} catch (err) {
await addLog(getErrorMessage(err), 'warn');
@@ -149,14 +359,40 @@
return record;
}
async function clearAccountRunHistory(stateOverride = null) {
const state = stateOverride || await getState();
const history = await getPersistedAccountRunHistory();
await setPersistedAccountRunHistory([]);
try {
const filePath = await syncAccountRunHistorySnapshot([], state);
if (filePath) {
await addLog(`账号记录快照已同步到本地:${filePath}`, 'info');
}
} catch (err) {
await addLog(getErrorMessage(err), 'warn');
}
return {
clearedCount: history.length,
};
}
return {
appendAccountRunRecord,
appendAccountRunHistoryRecord,
appendAccountRunHistoryTextFile,
buildAccountRunHistoryRecord,
buildAccountRunHistorySnapshotPayload,
clearAccountRunHistory,
getPersistedAccountRunHistory,
normalizeAccountRunHistory,
normalizeAccountRunHistoryRecord,
normalizeFinalStatus,
setPersistedAccountRunHistory,
shouldAppendAccountRunTextFile,
shouldSyncAccountRunHistorySnapshot,
summarizeAccountRunHistory,
syncAccountRunHistorySnapshot,
};
}
+13
View File
@@ -12,6 +12,7 @@
broadcastDataUpdate,
cancelScheduledAutoRun,
checkIcloudSession,
clearAccountRunHistory,
clearAutoRunTimerAlarm,
clearLuckmailRuntimeState,
clearStopRequest,
@@ -270,6 +271,18 @@
return { ok: true };
}
case 'CLEAR_ACCOUNT_RUN_HISTORY': {
const state = await getState();
if (isAutoRunLockedState(state)) {
throw new Error('自动流程运行中,当前不能清理邮箱记录。');
}
if (typeof clearAccountRunHistory !== 'function') {
return { ok: true, clearedCount: 0 };
}
const result = await clearAccountRunHistory(state);
return { ok: true, ...result };
}
case 'EXECUTE_STEP': {
clearStopRequest();
if (message.source === 'sidepanel') {
+104 -2
View File
@@ -64,7 +64,8 @@ REQUEST_TIMEOUT_SECONDS = 45
FETCH_LIMIT_DEFAULT = 5
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
ACCOUNT_LOG_PATH = os.path.join(BASE_DIR, "data", "account-run-history.txt")
ACCOUNT_LOG_LOCK = threading.Lock()
ACCOUNT_RECORDS_SNAPSHOT_PATH = os.path.join(BASE_DIR, "data", "account-run-history.json")
ACCOUNT_RECORDS_LOCK = threading.Lock()
def json_response(handler, status, payload):
@@ -131,12 +132,104 @@ def append_account_log(email_addr, password, status, recorded_at="", reason=""):
os.makedirs(os.path.dirname(ACCOUNT_LOG_PATH), exist_ok=True)
line = f"{normalized_recorded_at}\t{normalized_email}\t{normalized_password}\t{normalized_status}\t{normalized_reason}\n"
with ACCOUNT_LOG_LOCK:
with ACCOUNT_RECORDS_LOCK:
with open(ACCOUNT_LOG_PATH, "a", encoding="utf-8") as handle:
handle.write(line)
return ACCOUNT_LOG_PATH
def normalize_account_run_snapshot_record(record):
if not isinstance(record, dict):
return None
email_addr = str(record.get("email") or "").strip()
password = str(record.get("password") or "").strip()
final_status = str(record.get("finalStatus") or "").strip().lower()
if not email_addr or not password or final_status not in {"success", "failed"}:
return None
finished_at = str(record.get("finishedAt") or "").strip() or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
retry_count = max(0, int(record.get("retryCount") or 0))
failed_step_raw = record.get("failedStep")
try:
failed_step = int(failed_step_raw)
except (TypeError, ValueError):
failed_step = None
if failed_step is not None and failed_step <= 0:
failed_step = None
auto_run_context = record.get("autoRunContext") if isinstance(record.get("autoRunContext"), dict) else None
normalized_auto_run_context = None
if auto_run_context:
normalized_auto_run_context = {
"currentRun": max(0, int(auto_run_context.get("currentRun") or 0)),
"totalRuns": max(0, int(auto_run_context.get("totalRuns") or 0)),
"attemptRun": max(0, int(auto_run_context.get("attemptRun") or 0)),
}
if not any(normalized_auto_run_context.values()):
normalized_auto_run_context = None
source = "auto" if str(record.get("source") or "").strip().lower() == "auto" else "manual"
return {
"recordId": str(record.get("recordId") or email_addr).strip() or email_addr,
"email": email_addr,
"password": password,
"finalStatus": final_status,
"finishedAt": finished_at,
"retryCount": retry_count,
"failureLabel": str(record.get("failureLabel") or "").strip(),
"failureDetail": str(record.get("failureDetail") or "").strip(),
"failedStep": failed_step,
"source": source,
"autoRunContext": normalized_auto_run_context,
}
def summarize_account_run_snapshot(records):
summary = {
"total": 0,
"success": 0,
"failed": 0,
"retryTotal": 0,
}
for item in records:
summary["total"] += 1
if item.get("finalStatus") == "success":
summary["success"] += 1
elif item.get("finalStatus") == "failed":
summary["failed"] += 1
summary["retryTotal"] += max(0, int(item.get("retryCount") or 0))
return summary
def normalize_account_run_snapshot_payload(payload):
if not isinstance(payload, dict):
raise RuntimeError("Invalid account run snapshot payload")
normalized_records = []
for item in payload.get("records") if isinstance(payload.get("records"), list) else []:
normalized = normalize_account_run_snapshot_record(item)
if normalized:
normalized_records.append(normalized)
return {
"generatedAt": str(payload.get("generatedAt") or "").strip() or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
"summary": summarize_account_run_snapshot(normalized_records),
"records": normalized_records,
}
def sync_account_run_records(payload):
normalized_payload = normalize_account_run_snapshot_payload(payload)
os.makedirs(os.path.dirname(ACCOUNT_RECORDS_SNAPSHOT_PATH), exist_ok=True)
with ACCOUNT_RECORDS_LOCK:
with open(ACCOUNT_RECORDS_SNAPSHOT_PATH, "w", encoding="utf-8") as handle:
json.dump(normalized_payload, handle, ensure_ascii=False, indent=2)
handle.write("\n")
return ACCOUNT_RECORDS_SNAPSHOT_PATH
def try_refresh_access_token(endpoint, client_id, refresh_token):
request_data = {
"client_id": client_id,
@@ -625,6 +718,14 @@ class HotmailHelperHandler(BaseHTTPRequestHandler):
try:
payload = read_json_payload(self)
if self.path == "/sync-account-run-records":
file_path = sync_account_run_records(payload)
json_response(self, 200, {
"ok": True,
"filePath": file_path,
})
return
if self.path == "/append-account-log":
file_path = append_account_log(
payload.get("email"),
@@ -690,6 +791,7 @@ def main():
server = ThreadingHTTPServer((HOST, PORT), HotmailHelperHandler)
print(f"Hotmail helper listening on http://{HOST}:{PORT}", flush=True)
print(f"Account log file: {ACCOUNT_LOG_PATH}", flush=True)
print(f"Account snapshot file: {ACCOUNT_RECORDS_SNAPSHOT_PATH}", flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
+285
View File
@@ -0,0 +1,285 @@
(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));
let currentPage = 1;
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 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));
}
function summarizeAccountRunHistory(records = []) {
return records.reduce((summary, record) => {
summary.total += 1;
if (record.finalStatus === 'success') {
summary.success += 1;
} else if (record.finalStatus === 'failed') {
summary.failed += 1;
}
summary.retryTotal += Math.max(0, Math.floor(Number(record.retryCount) || 0));
return summary;
}, {
total: 0,
success: 0,
failed: 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 = {}) {
return record.finalStatus === 'success'
? { kind: 'success', label: '成功' }
: { kind: 'failed', label: '失败' };
}
function getRecordSummaryText(record = {}) {
if (record.finalStatus === 'success') {
return '流程完成';
}
return String(record.failureLabel || '').trim() || '流程失败';
}
function createStatChip(label, value, className = '') {
return `<span class="account-records-stat${className ? ` ${className}` : ''}"><strong>${escapeHtml(String(value))}</strong>${escapeHtml(label)}</span>`;
}
function updateHeader(records) {
if (!dom.accountRecordsMeta || !dom.accountRecordsStats) {
return;
}
if (!records.length) {
dom.accountRecordsMeta.textContent = '暂无邮箱记录';
} else {
dom.accountRecordsMeta.textContent = `${records.length} 条,最近更新于 ${formatAccountRecordTime(records[0]?.finishedAt)}`;
}
const summary = summarizeAccountRunHistory(records);
dom.accountRecordsStats.innerHTML = [
createStatChip('总', summary.total),
createStatChip('成', summary.success, 'is-success'),
createStatChip('失', summary.failed, 'is-failed'),
createStatChip('重试', summary.retryTotal, 'is-retry'),
].join('');
}
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;
}
if (dom.accountRecordsPageLabel) {
dom.accountRecordsPageLabel.textContent = totalPages > 0 ? `${currentPage} / ${totalPages}` : '0 / 0';
}
if (dom.btnAccountRecordsPrev) {
dom.btnAccountRecordsPrev.disabled = totalPages <= 1 || currentPage <= 1;
}
if (dom.btnAccountRecordsNext) {
dom.btnAccountRecordsNext.disabled = totalPages <= 1 || currentPage >= totalPages;
}
if (dom.btnClearAccountRecords) {
dom.btnClearAccountRecords.disabled = totalRecords === 0;
}
return totalPages;
}
function renderRecordList(records = []) {
if (!dom.accountRecordsList) {
return;
}
const totalPages = updatePagination(records.length);
if (!records.length) {
dom.accountRecordsList.innerHTML = '<div class="account-records-empty">暂无邮箱记录。</div>';
return;
}
const startIndex = (currentPage - 1) * pageSize;
const visibleRecords = records.slice(startIndex, startIndex + pageSize);
dom.accountRecordsList.innerHTML = visibleRecords.map((record) => {
const statusMeta = getStatusMeta(record);
const summaryText = getRecordSummaryText(record);
const retryCount = Math.max(0, Math.floor(Number(record.retryCount) || 0));
return `
<div class="account-record-item is-${escapeHtml(statusMeta.kind)}" title="${escapeHtml(String(record.email || ''))}">
<div class="account-record-item-top">
<div class="account-record-item-email mono">${escapeHtml(String(record.email || '').trim() || '(空邮箱)')}</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 && dom.accountRecordsPageLabel) {
dom.accountRecordsPageLabel.textContent = '1 / 1';
}
}
function render(currentState = state.getLatestState()) {
const records = getAccountRunRecords(currentState);
updateHeader(records);
renderRecordList(records);
}
function openPanel() {
if (dom.accountRecordsOverlay) {
dom.accountRecordsOverlay.hidden = false;
}
render();
}
function closePanel() {
if (dom.accountRecordsOverlay) {
dom.accountRecordsOverlay.hidden = true;
}
}
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);
}
currentPage = 1;
state.syncLatestState({ accountRunHistory: [] });
helpers.showToast?.(`已清理 ${Math.max(0, Number(response?.clearedCount) || 0)} 条邮箱记录`, 'success', 2200);
}
function bindEvents() {
dom.btnOpenAccountRecords?.addEventListener('click', () => {
openPanel();
});
dom.btnCloseAccountRecords?.addEventListener('click', () => {
closePanel();
});
dom.accountRecordsOverlay?.addEventListener('click', (event) => {
if (event.target === dom.accountRecordsOverlay) {
closePanel();
}
});
dom.btnAccountRecordsPrev?.addEventListener('click', () => {
if (currentPage <= 1) {
return;
}
currentPage -= 1;
render();
});
dom.btnAccountRecordsNext?.addEventListener('click', () => {
currentPage += 1;
render();
});
dom.btnClearAccountRecords?.addEventListener('click', () => {
clearRecords().catch((error) => {
helpers.showToast?.(`清理邮箱记录失败:${error.message}`, 'error');
});
});
}
function reset() {
currentPage = 1;
closePanel();
render();
}
return {
bindEvents,
closePanel,
openPanel,
render,
reset,
summarizeAccountRunHistory,
};
}
globalScope.SidepanelAccountRecordsManager = {
createAccountRecordsManager,
};
})(typeof window !== 'undefined' ? window : globalThis);
+133 -61
View File
@@ -1668,59 +1668,88 @@ header {
margin-bottom: 6px;
}
.account-run-history-strip {
margin-bottom: 8px;
padding: 10px 12px;
.log-header-actions {
display: flex;
flex-direction: column;
gap: 8px;
background:
linear-gradient(180deg,
color-mix(in srgb, var(--bg-base) 72%, var(--blue-soft)),
color-mix(in srgb, var(--bg-surface) 92%, var(--bg-base))
);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
align-items: center;
gap: 6px;
}
.account-run-history-head {
.account-records-overlay {
position: fixed;
inset: 0;
z-index: 1210;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
background: rgba(15, 17, 23, 0.32);
backdrop-filter: blur(2px);
}
.account-records-overlay[hidden] {
display: none !important;
}
.account-records-panel {
width: min(100%, 420px);
max-height: calc(100vh - 24px);
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px;
background:
linear-gradient(180deg,
color-mix(in srgb, var(--bg-base) 82%, var(--blue-soft)),
color-mix(in srgb, var(--bg-surface) 96%, var(--bg-base))
);
border: 1px solid var(--border);
border-radius: 14px;
box-shadow: var(--shadow-md), 0 18px 36px rgba(0, 0, 0, 0.18);
}
.account-records-panel-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.account-run-history-copy {
.account-records-panel-copy {
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.account-run-history-title {
font-size: 12px;
.account-records-panel-title {
font-size: 13px;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--text-primary);
}
.account-run-history-meta {
.account-records-panel-meta {
font-size: 11px;
color: var(--text-secondary);
line-height: 1.45;
}
.account-run-history-stats {
.account-records-panel-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
flex-wrap: wrap;
flex-shrink: 0;
}
.account-run-history-stat {
.account-records-stats {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.account-records-stat {
display: inline-flex;
align-items: center;
gap: 4px;
@@ -1733,55 +1762,80 @@ header {
color: var(--text-secondary);
}
.account-run-history-stat strong {
.account-records-stat strong {
font-family: 'JetBrains Mono', 'Consolas', monospace;
font-size: 10px;
}
.account-run-history-stat.is-success {
.account-records-stat.is-success {
color: var(--green);
background: var(--green-soft);
border-color: color-mix(in srgb, var(--green) 16%, var(--border));
}
.account-run-history-stat.is-failed {
.account-records-stat.is-failed {
color: var(--red);
background: var(--red-soft);
border-color: color-mix(in srgb, var(--red) 16%, var(--border));
}
.account-run-history-stat.is-stopped {
color: var(--cyan);
background: color-mix(in srgb, var(--cyan) 12%, var(--bg-base));
border-color: color-mix(in srgb, var(--cyan) 16%, var(--border));
.account-records-stat.is-retry {
color: var(--blue);
background: var(--blue-soft);
border-color: color-mix(in srgb, var(--blue) 16%, var(--border));
}
.account-run-history-list {
.account-records-list {
display: flex;
flex-direction: column;
gap: 6px;
min-height: 168px;
max-height: 360px;
overflow-y: auto;
padding-right: 2px;
}
.account-run-history-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
.account-records-list::-webkit-scrollbar { width: 5px; }
.account-records-list::-webkit-scrollbar-track { background: transparent; }
.account-records-list::-webkit-scrollbar-thumb { background: var(--bg-elevated); border-radius: 4px; }
.account-records-list::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
.account-records-empty {
min-height: 168px;
display: flex;
align-items: center;
padding: 8px 9px;
justify-content: center;
padding: 20px 12px;
border: 1px dashed var(--border);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--bg-base) 92%, transparent);
color: var(--text-secondary);
font-size: 12px;
text-align: center;
}
.account-record-item {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 11px;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--bg-base) 90%, transparent);
box-shadow: var(--shadow-sm);
}
.account-run-history-item-main {
min-width: 0;
.account-record-item-top,
.account-record-item-bottom {
display: flex;
flex-direction: column;
gap: 3px;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.account-run-history-item-email {
.account-record-item-email {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -1790,24 +1844,14 @@ header {
font-weight: 600;
}
.account-run-history-item-detail {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-secondary);
font-size: 11px;
line-height: 1.4;
}
.account-run-history-item-side {
.account-record-item-side {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.account-run-history-item-status {
.account-record-item-status {
display: inline-flex;
align-items: center;
padding: 2px 7px;
@@ -1818,36 +1862,64 @@ header {
color: var(--text-secondary);
}
.account-run-history-item-time {
.account-record-item-time {
color: var(--text-muted);
font-size: 11px;
}
.account-run-history-item.is-success {
.account-record-item-summary {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-secondary);
font-size: 11px;
line-height: 1.4;
}
.account-record-item-retry {
display: inline-flex;
align-items: center;
padding: 2px 7px;
border-radius: 999px;
background: color-mix(in srgb, var(--bg-base) 70%, var(--blue-soft));
color: var(--blue);
font-size: 11px;
font-weight: 700;
flex-shrink: 0;
}
.account-record-item.is-success {
border-color: color-mix(in srgb, var(--green) 14%, var(--border-subtle));
}
.account-run-history-item.is-success .account-run-history-item-status {
.account-record-item.is-success .account-record-item-status {
background: var(--green-soft);
color: var(--green);
}
.account-run-history-item.is-failed {
.account-record-item.is-failed {
border-color: color-mix(in srgb, var(--red) 14%, var(--border-subtle));
}
.account-run-history-item.is-failed .account-run-history-item-status {
.account-record-item.is-failed .account-record-item-status {
background: var(--red-soft);
color: var(--red);
}
.account-run-history-item.is-stopped {
border-color: color-mix(in srgb, var(--cyan) 14%, var(--border-subtle));
.account-records-pagination {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.account-run-history-item.is-stopped .account-run-history-item-status {
background: color-mix(in srgb, var(--cyan) 12%, var(--bg-base));
color: var(--cyan);
.account-records-page-label {
min-width: 56px;
text-align: center;
color: var(--text-secondary);
font-size: 11px;
}
#log-area {
+28 -12
View File
@@ -289,7 +289,7 @@
</div>
</div>
<div class="data-row">
<span class="data-label">本地留档</span>
<span class="data-label">本地同步</span>
<div class="data-inline auto-delay-inline">
<label class="toggle-switch" for="input-account-run-history-text-enabled">
<input type="checkbox" id="input-account-run-history-text-enabled" />
@@ -308,7 +308,7 @@
</div>
</div>
<div class="data-row" id="row-account-run-history-helper-base-url" style="display:none;">
<span class="data-label">留档服务</span>
<span class="data-label">同步服务</span>
<input type="text" id="input-account-run-history-helper-base-url" class="data-input mono" placeholder="http://127.0.0.1:17373" />
</div>
<div class="data-row">
@@ -555,21 +555,36 @@
<section id="log-section">
<div class="log-header">
<span class="section-label">日志</span>
<button id="btn-clear-log" class="btn btn-ghost btn-xs" title="清空日志">清空</button>
</div>
<div id="account-run-history-strip" class="account-run-history-strip" hidden>
<div class="account-run-history-head">
<div class="account-run-history-copy">
<span class="account-run-history-title">账号运行历史</span>
<span id="account-run-history-meta" class="account-run-history-meta">最近运行记录</span>
</div>
<div id="account-run-history-stats" class="account-run-history-stats"></div>
<div class="log-header-actions">
<button id="btn-open-account-records" class="btn btn-ghost btn-xs" title="查看邮箱记录">记录</button>
<button id="btn-clear-log" class="btn btn-ghost btn-xs" title="清空日志">清空</button>
</div>
<div id="account-run-history-list" class="account-run-history-list"></div>
</div>
<div id="log-area"></div>
</section>
<div id="account-records-overlay" class="account-records-overlay" hidden>
<div class="account-records-panel">
<div class="account-records-panel-header">
<div class="account-records-panel-copy">
<span class="account-records-panel-title">邮箱记录</span>
<span id="account-records-meta" class="account-records-panel-meta">暂无邮箱记录</span>
</div>
<div class="account-records-panel-actions">
<button id="btn-clear-account-records" class="btn btn-ghost btn-xs" type="button">清理记录</button>
<button id="btn-close-account-records" class="modal-close" type="button" aria-label="关闭">×</button>
</div>
</div>
<div id="account-records-stats" class="account-records-stats"></div>
<div id="account-records-list" class="account-records-list"></div>
<div class="account-records-pagination">
<button id="btn-account-records-prev" class="btn btn-outline btn-xs" type="button">上一页</button>
<span id="account-records-page-label" class="account-records-page-label mono">0 / 0</span>
<button id="btn-account-records-next" class="btn btn-outline btn-xs" type="button">下一页</button>
</div>
</div>
</div>
<div id="auto-start-modal" class="modal-overlay" hidden>
<div class="modal-card">
<div class="modal-header">
@@ -603,6 +618,7 @@
<script src="hotmail-manager.js"></script>
<script src="icloud-manager.js"></script>
<script src="luckmail-manager.js"></script>
<script src="account-records-manager.js"></script>
<script src="sidepanel.js"></script>
</body>
+48 -193
View File
@@ -11,10 +11,16 @@ const STATUS_ICONS = {
};
const logArea = document.getElementById('log-area');
const accountRunHistoryStrip = document.getElementById('account-run-history-strip');
const accountRunHistoryMeta = document.getElementById('account-run-history-meta');
const accountRunHistoryStats = document.getElementById('account-run-history-stats');
const accountRunHistoryList = document.getElementById('account-run-history-list');
const btnOpenAccountRecords = document.getElementById('btn-open-account-records');
const accountRecordsOverlay = document.getElementById('account-records-overlay');
const accountRecordsMeta = document.getElementById('account-records-meta');
const accountRecordsStats = document.getElementById('account-records-stats');
const accountRecordsList = document.getElementById('account-records-list');
const accountRecordsPageLabel = document.getElementById('account-records-page-label');
const btnAccountRecordsPrev = document.getElementById('btn-account-records-prev');
const btnAccountRecordsNext = document.getElementById('btn-account-records-next');
const btnCloseAccountRecords = document.getElementById('btn-close-account-records');
const btnClearAccountRecords = document.getElementById('btn-clear-account-records');
const updateSection = document.getElementById('update-section');
const extensionUpdateStatus = document.getElementById('extension-update-status');
const extensionVersionMeta = document.getElementById('extension-version-meta');
@@ -874,7 +880,7 @@ function syncLatestState(nextState) {
stepStatuses: mergedStepStatuses,
};
renderAccountRunHistory(latestState);
renderAccountRecords(latestState);
}
function hasOwnStateValue(source, key) {
@@ -1382,7 +1388,7 @@ function normalizeAccountRunHistoryHelperBaseUrlValue(value = '') {
return DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL;
}
if (parsed.pathname === '/append-account-log') {
if (parsed.pathname === '/append-account-log' || parsed.pathname === '/sync-account-run-records') {
parsed.pathname = '';
parsed.search = '';
parsed.hash = '';
@@ -2697,193 +2703,6 @@ function appendLog(entry) {
logArea.scrollTop = logArea.scrollHeight;
}
function getAccountRunHistory(state = latestState) {
return Array.isArray(state?.accountRunHistory)
? state.accountRunHistory.filter((item) => item && typeof item === 'object')
: [];
}
function parseAccountRunStatus(status = '') {
const normalized = String(status || '').trim().toLowerCase();
const stepMatch = normalized.match(/^step(\d+)_(failed|stopped)$/);
if (stepMatch) {
return {
kind: stepMatch[2] === 'failed' ? 'failed' : 'stopped',
label: `${stepMatch[1]}${stepMatch[2] === 'failed' ? '失败' : '停止'}`,
};
}
if (normalized === 'success') {
return { kind: 'success', label: '成功' };
}
if (normalized === 'failed') {
return { kind: 'failed', label: '失败' };
}
if (normalized === 'stopped') {
return { kind: 'stopped', label: '已停止' };
}
return {
kind: 'unknown',
label: normalized || '记录',
};
}
function summarizeAccountRunHistory(records = []) {
return records.reduce((summary, record) => {
summary.total += 1;
const { kind } = parseAccountRunStatus(record?.status);
if (kind === 'success') {
summary.success += 1;
} else if (kind === 'failed') {
summary.failed += 1;
} else if (kind === 'stopped') {
summary.stopped += 1;
} else {
summary.other += 1;
}
return summary;
}, {
total: 0,
success: 0,
failed: 0,
stopped: 0,
other: 0,
});
}
function formatAccountRunHistoryTime(recordedAt) {
const date = new Date(recordedAt);
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: DISPLAY_TIMEZONE,
});
}
return date.toLocaleString('zh-CN', {
hour12: false,
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
...(sameYear ? {} : { year: '2-digit' }),
timeZone: DISPLAY_TIMEZONE,
}).replace(/\//g, '-');
}
function buildAccountRunHistoryDetailText(record = {}) {
const reason = String(record.reason || '').trim();
if (reason) {
return reason;
}
const { kind } = parseAccountRunStatus(record.status);
if (kind === 'success') {
return '流程已完成并写入本地记录';
}
if (kind === 'stopped') {
return '流程被手动停止,已保留当前账号快照';
}
if (kind === 'failed') {
return '流程执行失败,已保留当前账号快照';
}
return '账号运行记录已保存';
}
function createAccountRunHistoryStat(label, value, className = '') {
const item = document.createElement('span');
item.className = `account-run-history-stat${className ? ` ${className}` : ''}`;
item.innerHTML = `<strong>${escapeHtml(String(value))}</strong>${escapeHtml(label)}`;
return item;
}
function renderAccountRunHistory(state = latestState) {
if (!accountRunHistoryStrip || !accountRunHistoryStats || !accountRunHistoryList || !accountRunHistoryMeta) {
return;
}
const records = getAccountRunHistory(state);
if (!records.length) {
accountRunHistoryStrip.hidden = true;
accountRunHistoryStats.innerHTML = '';
accountRunHistoryList.innerHTML = '';
accountRunHistoryMeta.textContent = '最近运行记录';
return;
}
const summary = summarizeAccountRunHistory(records);
const recentRecords = records.slice(-2).reverse();
const latestRecord = records[records.length - 1] || null;
accountRunHistoryStrip.hidden = false;
accountRunHistoryMeta.textContent = latestRecord
? `${summary.total} 条,最近更新于 ${formatAccountRunHistoryTime(latestRecord.recordedAt)}`
: `${summary.total}`;
accountRunHistoryStats.innerHTML = '';
accountRunHistoryStats.appendChild(createAccountRunHistoryStat('总', summary.total));
if (summary.success > 0) {
accountRunHistoryStats.appendChild(createAccountRunHistoryStat('成', summary.success, 'is-success'));
}
if (summary.failed > 0) {
accountRunHistoryStats.appendChild(createAccountRunHistoryStat('失', summary.failed, 'is-failed'));
}
if (summary.stopped > 0) {
accountRunHistoryStats.appendChild(createAccountRunHistoryStat('停', summary.stopped, 'is-stopped'));
}
accountRunHistoryList.innerHTML = '';
recentRecords.forEach((record) => {
const statusMeta = parseAccountRunStatus(record.status);
const item = document.createElement('div');
item.className = `account-run-history-item is-${statusMeta.kind}`;
item.title = [
record.email || '',
statusMeta.label,
record.reason || '',
].filter(Boolean).join('\n');
const main = document.createElement('div');
main.className = 'account-run-history-item-main';
const email = document.createElement('div');
email.className = 'account-run-history-item-email mono';
email.textContent = String(record.email || '').trim() || '(空邮箱)';
const detail = document.createElement('div');
detail.className = 'account-run-history-item-detail';
detail.textContent = buildAccountRunHistoryDetailText(record);
const side = document.createElement('div');
side.className = 'account-run-history-item-side';
const status = document.createElement('span');
status.className = 'account-run-history-item-status';
status.textContent = statusMeta.label;
const time = document.createElement('span');
time.className = 'account-run-history-item-time mono';
time.textContent = formatAccountRunHistoryTime(record.recordedAt);
main.append(email, detail);
side.append(status, time);
item.append(main, side);
accountRunHistoryList.appendChild(item);
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
@@ -3108,6 +2927,42 @@ const resetLuckmailManager = luckmailManager?.reset
const bindLuckmailEvents = luckmailManager?.bindLuckmailEvents
|| (() => { });
bindLuckmailEvents();
const accountRecordsManager = window.SidepanelAccountRecordsManager?.createAccountRecordsManager({
state: {
getLatestState: () => latestState,
syncLatestState,
},
dom: {
accountRecordsList,
accountRecordsMeta,
accountRecordsOverlay,
accountRecordsPageLabel,
accountRecordsStats,
btnAccountRecordsNext,
btnAccountRecordsPrev,
btnClearAccountRecords,
btnCloseAccountRecords,
btnOpenAccountRecords,
},
helpers: {
escapeHtml,
openConfirmModal,
showToast,
},
runtime: {
sendMessage: (message) => chrome.runtime.sendMessage(message),
},
constants: {
displayTimeZone: DISPLAY_TIMEZONE,
pageSize: 10,
},
});
const renderAccountRecords = accountRecordsManager?.render
|| (() => { });
const bindAccountRecordEvents = accountRecordsManager?.bindEvents
|| (() => { });
bindAccountRecordEvents();
renderStepsList();
async function exportSettingsFile() {
@@ -105,6 +105,10 @@ return {
api.normalizePersistentSettingValue('accountRunHistoryHelperBaseUrl', 'http://127.0.0.1:17373/append-account-log'),
'http://127.0.0.1:17373'
);
assert.equal(
api.normalizePersistentSettingValue('accountRunHistoryHelperBaseUrl', 'http://127.0.0.1:17373/sync-account-run-records'),
'http://127.0.0.1:17373'
);
assert.equal(
api.normalizeAccountRunHistoryHelperBaseUrl(''),
'http://127.0.0.1:17373'
@@ -16,12 +16,15 @@ test('account run history module exposes a factory', () => {
assert.equal(typeof api?.createAccountRunHistoryHelpers, 'function');
});
test('account run history helper normalizes records and persists without helper upload when local helper is disabled', async () => {
test('account run history helper upgrades old records, filters stopped items and stores normalized failed snapshot records', async () => {
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope);
let storedHistory = [{ email: 'old@example.com', password: 'old-pass', status: 'success', recordedAt: '2026-04-17T00:00:00.000Z' }];
let storedHistory = [
{ email: 'old@example.com', password: 'old-pass', status: 'success', recordedAt: '2026-04-17T00:00:00.000Z' },
{ email: 'stop@example.com', password: 'stop-pass', status: 'stopped', recordedAt: '2026-04-17T00:10:00.000Z' },
];
let fetchCalled = false;
global.fetch = async () => {
fetchCalled = true;
@@ -46,6 +49,10 @@ test('account run history helper normalizes records and persists without helper
getState: async () => ({
email: ' latest@example.com ',
password: ' secret ',
autoRunning: true,
autoRunCurrentRun: 2,
autoRunTotalRuns: 10,
autoRunAttemptRun: 3,
accountRunHistoryTextEnabled: false,
accountRunHistoryHelperBaseUrl: '',
}),
@@ -53,24 +60,133 @@ test('account run history helper normalizes records and persists without helper
});
const record = helpers.buildAccountRunHistoryRecord(
{ email: ' latest@example.com ', password: ' secret ' },
' FAILED ',
' reason '
{
email: ' latest@example.com ',
password: ' secret ',
autoRunning: true,
autoRunCurrentRun: 2,
autoRunTotalRuns: 10,
autoRunAttemptRun: 3,
},
'step8_failed',
'步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。'
);
assert.deepStrictEqual(record, {
recordId: 'latest@example.com',
email: 'latest@example.com',
password: 'secret',
status: 'failed',
recordedAt: record.recordedAt,
reason: 'reason',
finalStatus: 'failed',
finishedAt: record.finishedAt,
retryCount: 2,
failureLabel: '出现手机号验证',
failureDetail: '步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。',
failedStep: 8,
source: 'auto',
autoRunContext: {
currentRun: 2,
totalRuns: 10,
attemptRun: 3,
},
});
const appended = await helpers.appendAccountRunRecord('failed', null, 'boom');
const appended = await helpers.appendAccountRunRecord('step8_failed', null, '步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
assert.equal(appended.email, 'latest@example.com');
assert.equal(appended.status, 'failed');
assert.equal(storedHistory.length, 2);
assert.equal(storedHistory[1].reason, 'boom');
assert.equal(appended.finalStatus, 'failed');
assert.equal(appended.failureLabel, '出现手机号验证');
assert.equal(storedHistory.length, 2, '旧的 stopped 记录应在新结构中被过滤掉');
assert.equal(storedHistory.some((item) => item.email === 'stop@example.com'), false);
assert.equal(storedHistory.some((item) => item.email === 'latest@example.com' && item.retryCount === 2), true);
assert.equal(storedHistory.some((item) => item.email === 'old@example.com'), true);
assert.equal(fetchCalled, false);
assert.equal(helpers.shouldAppendAccountRunTextFile({ accountRunHistoryTextEnabled: false, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373' }), false);
assert.equal(helpers.shouldAppendAccountRunTextFile({ accountRunHistoryTextEnabled: true, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373' }), true);
assert.equal(helpers.buildAccountRunHistoryRecord({ email: 'a@b.com', password: 'x' }, 'stopped', 'stop'), null);
});
test('account run history helper clears persisted records and syncs full snapshot payload to local helper', async () => {
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope);
let storedHistory = [{
recordId: 'user@example.com',
email: 'user@example.com',
password: 'secret',
finalStatus: 'failed',
finishedAt: '2026-04-17T01:00:00.000Z',
retryCount: 1,
failureLabel: '步骤 6 失败',
failureDetail: '步骤 6:判断失败后已重试 2 次,仍未成功。',
failedStep: 6,
source: 'auto',
autoRunContext: {
currentRun: 1,
totalRuns: 5,
attemptRun: 2,
},
}];
const fetchCalls = [];
global.fetch = async (url, options = {}) => {
fetchCalls.push({
url,
options,
});
return {
ok: true,
json: async () => ({
ok: true,
filePath: 'C:/tmp/account-run-history.json',
}),
};
};
const logs = [];
const helpers = api.createAccountRunHistoryHelpers({
ACCOUNT_RUN_HISTORY_STORAGE_KEY: 'accountRunHistory',
addLog: async (message, level) => {
logs.push({ message, level });
},
buildLocalHelperEndpoint: (baseUrl, path) => `${baseUrl}${path}`,
chrome: {
storage: {
local: {
get: async () => ({ accountRunHistory: storedHistory }),
set: async (payload) => {
storedHistory = payload.accountRunHistory;
},
},
},
},
getErrorMessage: (error) => error?.message || String(error || ''),
getState: async () => ({
accountRunHistoryTextEnabled: true,
accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373',
}),
normalizeAccountRunHistoryHelperBaseUrl: (value) => String(value || '').trim(),
});
const payload = helpers.buildAccountRunHistorySnapshotPayload(storedHistory);
assert.deepStrictEqual(payload.summary, {
total: 1,
success: 0,
failed: 1,
retryTotal: 1,
});
const clearResult = await helpers.clearAccountRunHistory();
assert.deepStrictEqual(clearResult, { clearedCount: 1 });
assert.deepStrictEqual(storedHistory, []);
assert.equal(fetchCalls.length, 1);
assert.equal(fetchCalls[0].url, 'http://127.0.0.1:17373/sync-account-run-records');
assert.deepStrictEqual(JSON.parse(fetchCalls[0].options.body), {
generatedAt: JSON.parse(fetchCalls[0].options.body).generatedAt,
summary: {
total: 0,
success: 0,
failed: 0,
retryTotal: 0,
},
records: [],
});
assert.equal(logs[0].message, '账号记录快照已同步到本地:C:/tmp/account-run-history.json');
});
@@ -0,0 +1,190 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => sidepanelSource.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < sidepanelSource.length; i += 1) {
const ch = sidepanelSource[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
let depth = 0;
let end = braceStart;
for (; end < sidepanelSource.length; end += 1) {
const ch = sidepanelSource[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return sidepanelSource.slice(start, end);
}
function createButton() {
return {
disabled: false,
textContent: '',
hidden: false,
listeners: {},
addEventListener(type, handler) {
this.listeners[type] = handler;
},
};
}
function createContainer() {
return {
innerHTML: '',
textContent: '',
hidden: true,
listeners: {},
addEventListener(type, handler) {
this.listeners[type] = handler;
},
};
}
test('sidepanel html contains account records overlay and manager script', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const managerIndex = html.indexOf('<script src="account-records-manager.js"></script>');
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
assert.match(html, /id="btn-open-account-records"/);
assert.match(html, /id="account-records-overlay"/);
assert.match(html, /id="account-records-list"/);
assert.match(html, /id="account-records-stats"/);
assert.match(html, /id="btn-clear-account-records"/);
assert.notEqual(managerIndex, -1);
assert.notEqual(sidepanelIndex, -1);
assert.ok(managerIndex < sidepanelIndex);
});
test('sidepanel account records helper normalizes snapshot helper base url', () => {
const bundle = [
extractFunction('normalizeAccountRunHistoryHelperBaseUrlValue'),
].join('\n');
const api = new Function(`
const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = 'http://127.0.0.1:17373';
${bundle}
return { normalizeAccountRunHistoryHelperBaseUrlValue };
`)();
assert.equal(
api.normalizeAccountRunHistoryHelperBaseUrlValue('http://127.0.0.1:17373/sync-account-run-records'),
'http://127.0.0.1:17373'
);
});
test('account records manager exposes a factory and renders summarized paginated records', () => {
const source = fs.readFileSync('sidepanel/account-records-manager.js', 'utf8');
const windowObject = {};
const api = new Function('window', `${source}; return window.SidepanelAccountRecordsManager;`)(windowObject);
assert.equal(typeof api?.createAccountRecordsManager, 'function');
const btnOpenAccountRecords = createButton();
const btnCloseAccountRecords = createButton();
const btnClearAccountRecords = createButton();
const btnAccountRecordsPrev = createButton();
const btnAccountRecordsNext = createButton();
const overlay = createContainer();
const list = createContainer();
const stats = createContainer();
const meta = createContainer();
const pageLabel = createContainer();
const manager = api.createAccountRecordsManager({
state: {
getLatestState: () => ({
accountRunHistory: [
{
email: 'success@example.com',
password: 'secret',
finalStatus: 'success',
finishedAt: '2026-04-17T04:31:00.000Z',
retryCount: 0,
failureLabel: '流程完成',
},
{
email: 'failed@example.com',
password: 'secret',
finalStatus: 'failed',
finishedAt: '2026-04-17T04:29:00.000Z',
retryCount: 2,
failureLabel: '出现手机号验证',
},
],
}),
syncLatestState() {},
},
dom: {
accountRecordsList: list,
accountRecordsMeta: meta,
accountRecordsOverlay: overlay,
accountRecordsPageLabel: pageLabel,
accountRecordsStats: stats,
btnAccountRecordsNext,
btnAccountRecordsPrev,
btnClearAccountRecords,
btnCloseAccountRecords,
btnOpenAccountRecords,
},
helpers: {
escapeHtml: (value) => String(value || ''),
openConfirmModal: async () => true,
showToast() {},
},
runtime: {
sendMessage: async () => ({ clearedCount: 2 }),
},
constants: {
displayTimeZone: 'Asia/Shanghai',
pageSize: 10,
},
});
assert.equal(typeof manager.bindEvents, 'function');
assert.equal(typeof manager.render, 'function');
assert.equal(typeof manager.openPanel, 'function');
manager.bindEvents();
manager.render();
assert.match(meta.textContent, /共 2 条/);
assert.match(stats.innerHTML, /重试/);
assert.match(list.innerHTML, /success@example\.com/);
assert.match(list.innerHTML, /出现手机号验证/);
assert.match(list.innerHTML, /重试 2/);
assert.equal(pageLabel.textContent, '1 / 1');
assert.equal(btnClearAccountRecords.disabled, false);
});
-118
View File
@@ -1,118 +0,0 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => sidepanelSource.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < sidepanelSource.length; i += 1) {
const ch = sidepanelSource[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
let depth = 0;
let end = braceStart;
for (; end < sidepanelSource.length; end += 1) {
const ch = sidepanelSource[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return sidepanelSource.slice(start, end);
}
test('sidepanel html contains account run history strip under log header', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="account-run-history-strip"/);
assert.match(html, /id="account-run-history-meta"/);
assert.match(html, /id="account-run-history-stats"/);
assert.match(html, /id="account-run-history-list"/);
assert.match(html, /id="input-account-run-history-text-enabled"/);
assert.match(html, /id="input-account-run-history-helper-base-url"/);
});
test('sidepanel account run history helpers classify statuses and summarize counts', () => {
const bundle = [
extractFunction('normalizeAccountRunHistoryHelperBaseUrlValue'),
extractFunction('parseAccountRunStatus'),
extractFunction('summarizeAccountRunHistory'),
extractFunction('buildAccountRunHistoryDetailText'),
].join('\n');
const api = new Function(`
const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = 'http://127.0.0.1:17373';
${bundle}
return { normalizeAccountRunHistoryHelperBaseUrlValue, parseAccountRunStatus, summarizeAccountRunHistory, buildAccountRunHistoryDetailText };
`)();
assert.deepStrictEqual(api.parseAccountRunStatus('step7_failed'), {
kind: 'failed',
label: '步7失败',
});
assert.deepStrictEqual(api.parseAccountRunStatus('step4_stopped'), {
kind: 'stopped',
label: '步4停止',
});
assert.deepStrictEqual(api.parseAccountRunStatus('success'), {
kind: 'success',
label: '成功',
});
assert.deepStrictEqual(api.summarizeAccountRunHistory([
{ status: 'success' },
{ status: 'step7_failed' },
{ status: 'stopped' },
{ status: 'step2_failed' },
]), {
total: 4,
success: 1,
failed: 2,
stopped: 1,
other: 0,
});
assert.equal(
api.buildAccountRunHistoryDetailText({ status: 'success', reason: '' }),
'流程已完成并写入本地记录'
);
assert.equal(
api.buildAccountRunHistoryDetailText({ status: 'step7_failed', reason: '' }),
'流程执行失败,已保留当前账号快照'
);
assert.equal(
api.buildAccountRunHistoryDetailText({ status: 'stopped', reason: '手动停止' }),
'手动停止'
);
assert.equal(
api.normalizeAccountRunHistoryHelperBaseUrlValue('http://127.0.0.1:17373/append-account-log'),
'http://127.0.0.1:17373'
);
});
+7 -6
View File
@@ -37,7 +37,7 @@
- 向后台发送命令
- 接收后台广播并更新 UI
- 动态渲染步骤列表
- 在日志区标题下方汇总展示最近账号运行历史
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/重试统计与分页列表
### 2.2 Background Service Worker
@@ -137,9 +137,9 @@
- LuckMail API 配置
- 自动运行默认配置
- 账号运行历史 `accountRunHistory`
- 账号运行历史 txt 留档开关与 helper 地址
- 账号运行历史本地同步开关与 helper 地址
当启用了独立的账号运行历史 txt 留档配置时,账号运行历史会通过 [scripts/hotmail_helper.py](c:/Users/projectf/Downloads/codex注册扩展/scripts/hotmail_helper.py) 追加写入 `data/account-run-history.txt` 文本文件,便于离线留档
当启用了独立的账号运行历史本地同步配置时,账号运行历史会通过 [scripts/hotmail_helper.py](c:/Users/projectf/Downloads/codex注册扩展/scripts/hotmail_helper.py) 整体同步写入 `data/account-run-history.json` 快照文件,便于开发者直接查看完整记录
这条配置链路独立于 `mailProvider` 和 Hotmail 的接码模式。
### 4.3 状态广播
@@ -408,8 +408,8 @@
补充:
- 本地 helper 除了收信与验证码读取,还提供账号运行历史文本追加接口。
- 账号运行历史 txt 留档由独立配置控制,不再绑定 Hotmail 的本地助手模式。
- 本地 helper 除了收信与验证码读取,还提供邮箱记录 JSON 快照同步接口。
- 账号运行历史本地同步由独立配置控制,不再绑定 Hotmail 的本地助手模式。
### 7.3 LuckMail
@@ -444,7 +444,8 @@
- 立即停止
- 当前轮重试
- 下一轮继续
6. 当前轮最终失败或被停止时,追加账号运行历史记录
6. 当前轮最终失败时,写入邮箱记录,并在自动重试链路中累计重试次数
- 手动停止与自动停止不再进入新邮箱记录体系
7. 如果配置了线程间隔,则挂计时计划
8. 所有轮次结束后输出汇总
+9 -8
View File
@@ -31,8 +31,8 @@
- `microsoft-email.js`Microsoft Graph / Outlook 邮件读取辅助模块,负责刷新令牌换 token、邮箱夹轮询和验证码提取。
- `package.json`:仓库最小 Node 包配置,目前主要提供测试脚本定义。
- `rules.json`:静态 DNR 规则,主要处理 iCloud 相关请求头。
- `start-hotmail-helper.bat`Windows 下启动本地 helper 的脚本;当前既服务于 Hotmail 本地收信,也服务于账号运行历史文本留档
- `start-hotmail-helper.command`macOS 下启动本地 helper 的脚本;当前既服务于 Hotmail 本地收信,也服务于账号运行历史文本留档
- `start-hotmail-helper.bat`Windows 下启动本地 helper 的脚本;当前既服务于 Hotmail 本地收信,也服务于邮箱记录快照同步
- `start-hotmail-helper.command`macOS 下启动本地 helper 的脚本;当前既服务于 Hotmail 本地收信,也服务于邮箱记录快照同步
- `开发者AI开发与PR提交流程.md`:仓库现有的 AI 开发与 PR 提交流程说明。
- `项目文件结构说明.md`:当前文件,维护整个仓库非忽略文件的结构与职责索引。
- `项目完整链路说明.md`:面向 AI/开发者的完整功能链路说明,用于快速理解系统整体运行过程。
@@ -40,7 +40,7 @@
## `background/`
- `background/account-run-history.js`账号运行历史模块,负责将成功失败、停止结果持久化`chrome.storage.local`,并在启用独立本地 txt 留档配置后追加写入文本日志
- `background/account-run-history.js`邮箱记录模块,负责把旧的 append-only 结果流水归一化为成功/失败摘要记录,落地`chrome.storage.local`支持清理记录,并在启用独立本地同步配置后把完整快照同步到本地 helper
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;fresh-attempt reset 时会额外保留 `gmailBaseEmail``mail2925BaseEmail`,避免自动流程重置后丢失别名基邮箱配置。
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口。
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层。
@@ -105,16 +105,17 @@
## `scripts/`
- `scripts/hotmail_helper.py`:本地 helper 服务,负责通过本地接口协助 Hotmail 获取邮件和验证码,并提供账号运行历史文本追加接口
- `scripts/hotmail_helper.py`:本地 helper 服务,负责通过本地接口协助 Hotmail 获取邮件和验证码,并提供邮箱记录 JSON 快照同步接口;旧的文本追加接口仍保留作兼容
## `sidepanel/`
- `sidepanel/hotmail-manager.js`:侧边栏 Hotmail 账号池管理器,负责列表、导入、验证、测试收信和批量操作。
- `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。
- `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、统计摘要和清理确认。
- `sidepanel/sidepanel.css`:侧边栏样式文件。
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区标题下方预留账号运行历史摘要条带,并新增共享“验证码重发”次数输入,同时会加载 `managed-alias-utils.js`,把旧的“邮箱前缀”字段语义改为“别名基邮箱”。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、账号运行历史摘要渲染、独立 txt 留档配置、共享验证码自动重发次数配置与广播接收;同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上。
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,同时新增共享“验证码重发”次数输入,加载 `managed-alias-utils.js`,把旧的“邮箱前缀”字段语义改为“别名基邮箱”。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / iCloud / LuckMail / 邮箱记录面板 manager;同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上。
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询与版本展示。
## `tests/`
@@ -125,7 +126,7 @@
- `tests/auto-run-step6-restart.test.js`:测试自动运行在后半段授权链路遇错时会回到步骤 7 重开,并在命中 add-phone 时停止重开。
- `tests/auto-step-random-delay.test.js`:测试自动运行步间延迟与旧配置键兼容解析。
- `tests/background-auto-run-module.test.js`:测试自动运行控制器模块已接入且导出工厂。
- `tests/background-account-run-history-module.test.js`:测试账号运行历史模块已接入、导出工厂,并能在不启用本地 helper 时正确持久化记录
- `tests/background-account-run-history-module.test.js`:测试邮箱记录模块已接入、导出工厂,能够过滤停止记录、归一化失败标签、计算自动重试次数,并支持清理后同步完整快照
- `tests/background-account-history-settings.test.js`:测试账号运行历史的独立配置项归一化,不再复用 Hotmail 模式作为启停条件。
- `tests/background-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂。
- `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。
@@ -156,7 +157,7 @@
- `tests/managed-alias-utils.test.js`:覆盖 Gmail / 2925 共享别名工具的解析、生成、兼容性判断。
- `tests/microsoft-email.test.js`:测试 Microsoft 邮件拉取与验证码提取逻辑。
- `tests/sidepanel-hotmail-manager.test.js`:测试侧边栏 Hotmail 管理器模块接线与空态渲染。
- `tests/sidepanel-account-run-history.test.js`:测试侧边栏日志区下方的账号运行历史条带结构、独立留档配置控件与状态分类摘要逻辑。
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏邮箱记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
- `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。
- `tests/sidepanel-icloud-provider.test.js`:测试侧边栏 iCloud 登录地址解析逻辑。
- `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。