修改日志显示,合并同一轮中对应的邮箱和手机号

This commit is contained in:
QLHazyCoder
2026-05-05 03:59:09 +08:00
parent c62e1746f8
commit 24f5b79bf1
14 changed files with 377 additions and 42 deletions
+3 -3
View File
@@ -48,8 +48,8 @@
- 页面要求填写 `age`
- 支持 `Auto` 多轮运行
- 支持中途 `Stop`
- 支持通过日志区的 `记录` 按钮查看邮箱记录面板,按邮箱展示最终状态、时间、失败标签和重试次数
- 支持将邮箱记录完整快照同步到本地 helper,便于开发者直接查看 `data/account-run-history.json`
- 支持通过日志区的 `记录` 按钮查看账号记录面板,同一轮的邮箱和手机号会合并显示,按最终状态、时间、失败标签和重试次数筛选
- 支持将账号记录完整快照同步到本地 helper,便于开发者直接查看 `data/account-run-history.json`
- Step 8 会自动寻找 OAuth 同意页的“继续”按钮,并通过 Chrome debugger 输入事件发起点击,然后监听本地回调地址
@@ -308,7 +308,7 @@ python3 scripts/hotmail_helper.py
Hotmail helper listening on http://127.0.0.1:17373
```
同时还会输出本地邮箱记录快照文件路径。看到这些输出后,再回到扩展里点 `校验``复制最新验证码`邮箱记录快照会按默认本地 helper 地址自动同步,无需再手动开启本地同步。
同时还会输出本地账号记录快照文件路径。看到这些输出后,再回到扩展里点 `校验``复制最新验证码`账号记录快照会按默认本地 helper 地址自动同步,无需再手动开启本地同步。
#### 最小排错说明
+3
View File
@@ -801,6 +801,7 @@ const DEFAULT_STATE = {
currentLuckmailPurchase: null,
currentLuckmailMailCursor: null,
currentPhoneActivation: null,
phoneNumber: '',
currentPhoneVerificationCode: '',
currentPhoneVerificationCountdownEndsAt: 0,
currentPhoneVerificationCountdownWindowIndex: 0,
@@ -2694,6 +2695,7 @@ async function setEmailStateSilently(email) {
if (normalizedEmail) {
updates.accountIdentifierType = 'email';
updates.accountIdentifier = normalizedEmail;
updates.phoneNumber = '';
updates.signupPhoneNumber = '';
updates.signupPhoneActivation = null;
updates.signupPhoneCompletedActivation = null;
@@ -2726,6 +2728,7 @@ async function setSignupPhoneStateSilently(phoneNumber) {
if (normalizedPhoneNumber) {
updates.accountIdentifierType = 'phone';
updates.accountIdentifier = normalizedPhoneNumber;
updates.phoneNumber = '';
if (!isPhoneActivationForNumber(currentState?.signupPhoneActivation, normalizedPhoneNumber)) {
updates.signupPhoneActivation = null;
updates.signupPhoneVerificationRequestedAt = null;
+48 -10
View File
@@ -104,26 +104,64 @@
: normalizedValue.toLowerCase();
}
function getActivationPhoneNumber(activation = null) {
if (!activation || typeof activation !== 'object' || Array.isArray(activation)) {
return '';
}
return String(
activation.phoneNumber
?? activation.number
?? activation.phone
?? ''
).trim();
}
function resolveStatePhoneNumber(state = {}) {
const identifierType = String(state?.accountIdentifierType || '').trim().toLowerCase();
const accountIdentifierPhone = identifierType === 'phone'
? String(state?.accountIdentifier || '').trim()
: '';
return String(
state?.phoneNumber
|| state?.signupPhoneNumber
|| accountIdentifierPhone
|| getActivationPhoneNumber(state?.signupPhoneCompletedActivation)
|| getActivationPhoneNumber(state?.signupPhoneActivation)
|| getActivationPhoneNumber(state?.currentPhoneActivation)
|| ''
).trim();
}
function normalizePhoneRecordKey(value = '') {
const rawValue = String(value || '').trim();
const digits = rawValue.replace(/\D+/g, '');
return digits || rawValue.toLowerCase();
}
function resolveRecordIdentity(record = {}) {
const email = String(record.email || '').trim().toLowerCase();
const phoneNumber = String(record.phoneNumber ?? record.phone ?? record.number ?? '').trim();
const rawEmail = String(record.email || '').trim().toLowerCase();
const rawPhoneNumber = String(record.phoneNumber ?? record.phone ?? record.number ?? '').trim();
const rawIdentifierType = String(record.accountIdentifierType || '').trim().toLowerCase();
const inferredIdentifierType = rawIdentifierType === 'phone'
|| (!email && phoneNumber)
? 'phone'
: 'email';
: (rawIdentifierType === 'email'
? 'email'
: ((!rawEmail && rawPhoneNumber) ? 'phone' : 'email'));
const rawAccountIdentifier = String(
record.accountIdentifier
|| (inferredIdentifierType === 'phone' ? phoneNumber : email)
|| (inferredIdentifierType === 'phone' ? rawPhoneNumber : rawEmail)
|| ''
).trim();
const accountIdentifierType = rawAccountIdentifier
? normalizeAccountIdentifierType(inferredIdentifierType)
: (email ? 'email' : (phoneNumber ? 'phone' : ''));
: (rawEmail ? 'email' : (rawPhoneNumber ? 'phone' : ''));
const accountIdentifier = normalizeAccountIdentifierValue(
rawAccountIdentifier || (accountIdentifierType === 'phone' ? phoneNumber : email),
rawAccountIdentifier || (accountIdentifierType === 'phone' ? rawPhoneNumber : rawEmail),
accountIdentifierType || inferredIdentifierType
);
const email = rawEmail || (accountIdentifierType === 'email' ? accountIdentifier : '');
const phoneNumber = rawPhoneNumber || (accountIdentifierType === 'phone' ? accountIdentifier : '');
return {
email,
@@ -279,7 +317,7 @@
accountIdentifierType: state.accountIdentifierType,
accountIdentifier: state.accountIdentifier,
email: state.email,
phoneNumber: state.phoneNumber || state.signupPhoneNumber,
phoneNumber: resolveStatePhoneNumber(state),
});
const email = identity.email;
const phoneNumber = identity.phoneNumber;
@@ -329,7 +367,7 @@
const recordId = String(record.recordId || '').trim();
const emailKey = String(record.email || '').trim().toLowerCase();
const phoneKey = String(record.phoneNumber || '').trim().toLowerCase();
const phoneKey = normalizePhoneRecordKey(record.phoneNumber);
const identifierKey = buildRecordId(
record.accountIdentifier || record.email || record.phoneNumber,
record.accountIdentifierType || (phoneKey && !emailKey ? 'phone' : 'email')
@@ -337,7 +375,7 @@
const nextHistory = normalizedHistory.filter((item) => {
const itemRecordId = String(item.recordId || '').trim();
const itemEmailKey = String(item.email || '').trim().toLowerCase();
const itemPhoneKey = String(item.phoneNumber || '').trim().toLowerCase();
const itemPhoneKey = normalizePhoneRecordKey(item.phoneNumber);
const itemIdentifierKey = buildRecordId(
item.accountIdentifier || item.email || item.phoneNumber,
item.accountIdentifierType || (itemPhoneKey && !itemEmailKey ? 'phone' : 'email')
+1
View File
@@ -216,6 +216,7 @@
await setEmailState(email);
}
const updates = {
phoneNumber: '',
signupPhoneNumber: '',
signupPhoneActivation: null,
signupPhoneCompletedActivation: null,
+3
View File
@@ -5312,6 +5312,9 @@
clearCountrySmsFailure(activation.countryId, activation.provider);
shouldCancelActivation = false;
await clearCurrentActivation();
await setPhoneRuntimeState({
phoneNumber: activation.phoneNumber,
});
addPhoneReentryWithSameActivation = 0;
await addLog('步骤 9:手机号验证已完成,等待 OAuth 授权页。', 'ok');
return submitResult;
+80 -9
View File
@@ -72,8 +72,15 @@
if (rawRecordId) {
return rawRecordId.toLowerCase();
}
const identifierType = String(record.accountIdentifierType || '').trim().toLowerCase() === 'phone'
|| (!record.email && (record.accountIdentifier || record.phoneNumber))
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(
@@ -89,12 +96,71 @@
: identifier.toLowerCase();
}
function getRecordPrimaryIdentifier(record = {}) {
const email = String(record.email || '').trim();
if (email) {
return email;
function getRecordIdentifierType(record = {}) {
const rawType = String(record.accountIdentifierType || '').trim().toLowerCase();
if (rawType === 'phone') {
return 'phone';
}
return String(record.accountIdentifier || record.phoneNumber || record.phone || record.number || '').trim();
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()) {
@@ -382,6 +448,8 @@
dom.accountRecordsList.innerHTML = visibleRecords.map((record) => {
const recordId = buildRecordId(record);
const primaryIdentifier = getRecordPrimaryIdentifier(record) || '(空账号)';
const secondaryIdentifier = getRecordSecondaryIdentifier(record);
const recordTitle = getRecordTitle(record);
const statusMeta = getStatusMeta(record);
const summaryText = getRecordSummaryText(record);
const retryCount = normalizeRetryCount(record.retryCount);
@@ -408,12 +476,15 @@
<div
class="${itemClassNames}"
data-account-record-id="${escapeHtml(recordId)}"
title="${escapeHtml(primaryIdentifier)}"
title="${escapeHtml(recordTitle)}"
>
<div class="account-record-item-top">
<div class="account-record-item-email-row">
${selectionMarkup}
<div class="account-record-item-email mono">${escapeHtml(primaryIdentifier)}</div>
<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>
+18 -1
View File
@@ -3038,17 +3038,34 @@ header {
cursor: pointer;
}
.account-record-item-email {
.account-record-item-identity {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.account-record-item-email,
.account-record-item-secondary {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.account-record-item-email {
color: var(--text-primary);
font-size: 12px;
font-weight: 600;
}
.account-record-item-secondary {
color: var(--text-secondary);
font-size: 10px;
font-weight: 600;
}
.account-record-item-side {
display: flex;
align-items: center;
+4 -4
View File
@@ -1525,7 +1525,7 @@
<div class="log-header">
<span class="section-label">日志</span>
<div class="log-header-actions">
<button id="btn-open-account-records" class="btn btn-ghost btn-xs" title="查看邮箱记录">记录</button>
<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>
@@ -1536,15 +1536,15 @@
<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>
<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-close-account-records" class="modal-close" type="button" aria-label="关闭">×</button>
</div>
</div>
<div class="account-records-toolbar">
<div id="account-records-stats" class="account-records-stats" role="group" aria-label="邮箱记录筛选"></div>
<div id="account-records-stats" class="account-records-stats" role="group" aria-label="账号记录筛选"></div>
<div class="account-records-toolbar-actions">
<button id="btn-toggle-account-records-selection" class="btn btn-ghost btn-xs" type="button">多选</button>
<button id="btn-delete-selected-account-records" class="btn btn-ghost btn-xs" type="button" hidden
+1
View File
@@ -7238,6 +7238,7 @@ async function persistSignupPhoneInputValue(options = {}) {
signupPhoneInputDirty = getSignupPhoneInputValue() !== normalizedPhone;
syncLatestState({
signupPhoneNumber: normalizedPhone,
phoneNumber: '',
...(normalizedPhone
? {
accountIdentifierType: 'phone',
@@ -198,6 +198,132 @@ test('account run history helper accepts phone-only records without forcing emai
assert.equal(normalized.finalStatus, 'failed');
});
test('account run history merges email and phone identities from the same run', 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: 'phone:+447799342687',
accountIdentifierType: 'phone',
accountIdentifier: '+447799342687',
phoneNumber: '+44 7799 342687',
email: '',
password: '',
finalStatus: 'stopped',
finishedAt: '2026-04-17T04:30:00.000Z',
failureDetail: '步骤 2 已使用手机号,流程尚未完成。',
},
{
recordId: 'tmp@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'tmp@example.com',
email: 'tmp@example.com',
phoneNumber: '',
password: 'old',
finalStatus: 'stopped',
finishedAt: '2026-04-17T04:31:00.000Z',
failureDetail: '步骤 2 已使用邮箱,流程尚未完成。',
},
];
const helpers = api.createAccountRunHistoryHelpers({
chrome: {
storage: {
local: {
get: async () => ({ accountRunHistory: storedHistory }),
set: async (payload) => {
storedHistory = payload.accountRunHistory;
},
},
},
},
getState: async () => ({}),
normalizeAccountRunHistoryHelperBaseUrl: () => '',
});
const failedRecord = helpers.buildAccountRunHistoryRecord({
accountIdentifierType: 'email',
accountIdentifier: 'tmp@example.com',
email: 'tmp@example.com',
password: 'secret',
currentPhoneActivation: {
activationId: 'a1',
phoneNumber: '+44 7799 342687',
},
}, 'step9_failed', '步骤 9:手机号验证失败。');
assert.equal(failedRecord.accountIdentifierType, 'email');
assert.equal(failedRecord.accountIdentifier, 'tmp@example.com');
assert.equal(failedRecord.phoneNumber, '+44 7799 342687');
const successRecord = await helpers.appendAccountRunRecord('success', {
accountIdentifierType: 'email',
accountIdentifier: 'tmp@example.com',
email: 'tmp@example.com',
phoneNumber: '447799342687',
password: 'secret',
accountRunHistoryHelperBaseUrl: '',
});
assert.equal(successRecord.recordId, 'tmp@example.com');
assert.equal(successRecord.email, 'tmp@example.com');
assert.equal(successRecord.phoneNumber, '447799342687');
assert.equal(storedHistory.length, 1);
assert.equal(storedHistory[0].recordId, 'tmp@example.com');
assert.equal(storedHistory[0].finalStatus, 'success');
});
test('account run history keeps phone as primary identity when phone signup later binds email', 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: 'phone:+447700900123',
accountIdentifierType: 'phone',
accountIdentifier: '+447700900123',
phoneNumber: '+447700900123',
email: '',
finalStatus: 'stopped',
finishedAt: '2026-04-17T04:31:00.000Z',
failureDetail: '步骤 2 已使用手机号,流程尚未完成。',
}];
const helpers = api.createAccountRunHistoryHelpers({
chrome: {
storage: {
local: {
get: async () => ({ accountRunHistory: storedHistory }),
set: async (payload) => {
storedHistory = payload.accountRunHistory;
},
},
},
},
getState: async () => ({}),
normalizeAccountRunHistoryHelperBaseUrl: () => '',
});
const record = await helpers.appendAccountRunRecord('success', {
accountIdentifierType: 'phone',
accountIdentifier: '+447700900123',
signupPhoneNumber: '+447700900123',
email: 'bound@example.com',
password: 'secret',
accountRunHistoryHelperBaseUrl: '',
});
assert.equal(record.recordId, 'phone:+447700900123');
assert.equal(record.accountIdentifierType, 'phone');
assert.equal(record.accountIdentifier, '+447700900123');
assert.equal(record.email, 'bound@example.com');
assert.equal(record.phoneNumber, '+447700900123');
assert.equal(storedHistory.length, 1);
assert.equal(storedHistory[0].recordId, 'phone:+447700900123');
assert.equal(storedHistory[0].finalStatus, 'success');
});
test('account run history records preserve Plus and contribution mode flags', () => {
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
const globalScope = {};
+1
View File
@@ -1047,6 +1047,7 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
stateUpdates.some((entry) => entry?.currentPhoneActivation === null && entry?.currentPhoneVerificationCode === ''),
true
);
assert.equal(currentState.phoneNumber, '447911123456');
const actions = requests.map((url) => url.searchParams.get('action'));
assert.deepStrictEqual(actions, [
'getPrices',
@@ -407,3 +407,77 @@ test('account records manager displays phone-only records with account identifie
assert.match(list.innerHTML, /\+6612345/);
assert.doesNotMatch(list.innerHTML, /\(空邮箱\)/);
});
test('account records manager displays combined email and phone identities in one record', () => {
const source = fs.readFileSync('sidepanel/account-records-manager.js', 'utf8');
const windowObject = {};
const api = new Function('window', `${source}; return window.SidepanelAccountRecordsManager;`)(windowObject);
const list = createNode();
const manager = api.createAccountRecordsManager({
state: {
getLatestState: () => ({
accountRunHistory: [
{
recordId: 'phone:+447700900123',
accountIdentifierType: 'phone',
accountIdentifier: '+447700900123',
phoneNumber: '+447700900123',
email: 'bound@example.com',
finalStatus: 'success',
finishedAt: '2026-04-17T04:31:00.000Z',
retryCount: 0,
failureLabel: '流程完成',
},
{
recordId: 'mail@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'mail@example.com',
phoneNumber: '447799342687',
email: 'mail@example.com',
finalStatus: 'failed',
finishedAt: '2026-04-17T04:30:00.000Z',
retryCount: 0,
failureLabel: '步骤 9 失败',
},
],
}),
syncLatestState() {},
},
dom: {
accountRecordsList: list,
accountRecordsMeta: createNode(),
accountRecordsOverlay: createNode(),
accountRecordsPageLabel: createNode(),
accountRecordsStats: createNode(),
btnAccountRecordsNext: createNode(),
btnAccountRecordsPrev: createNode(),
btnClearAccountRecords: createNode(),
btnCloseAccountRecords: createNode(),
btnDeleteSelectedAccountRecords: createNode(),
btnOpenAccountRecords: createNode(),
btnToggleAccountRecordsSelection: createNode(),
},
helpers: {
escapeHtml: (value) => String(value || ''),
},
runtime: {
sendMessage: async () => ({}),
},
constants: {
displayTimeZone: 'Asia/Shanghai',
pageSize: 10,
},
});
manager.render();
assert.match(list.innerHTML, /\+447700900123/);
assert.match(list.innerHTML, /邮箱 bound@example\.com/);
assert.match(list.innerHTML, /mail@example\.com/);
assert.match(list.innerHTML, /绑定手机号 447799342687/);
assert.match(
list.innerHTML,
/title="\+447700900123 \/ 邮箱 bound@example\.com"/
);
});
+5 -5
View File
@@ -41,7 +41,7 @@
- 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态
- 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `apikey.qzz.io` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现
- 在 sidepanel 初始化和点击“自动”按钮前刷新一次贡献站公开内容摘要;点击“自动”时会先冻结当时输入或号池锁定的目标轮数,后续刷新、保存和后台状态回灌不能把启动目标改回旧的 1 轮;如果刷新失败,不阻塞主自动流程
- 在日志区通过“记录”按钮打开独立的账号记录覆盖层,并展示成功/失败/停止/重试统计与分页列表;phone-only 记录会回退展示手机号
- 在日志区通过“记录”按钮打开独立的账号记录覆盖层,并展示成功/失败/停止/重试统计与分页列表;同一轮同时出现邮箱和手机号时只显示一条组合记录,按本轮 `accountIdentifierType` 展示主身份,另一种身份作为副行显示
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro``v` 版本误显示为比 `Ultra` 更新
- 展示一个单独的“接码”开关、注册方式 `signupMethod` 与“接码平台”下拉;接码平台当前支持 HeroSMS / 5sim / NexSMS。普通模式下开启接码后可把注册方式切到手机号注册,并在 OAuth 登录链路命中手机号登录验证码页时继续复用同一手机号续跑短信验证
- 侧栏在接码卡内提供一个独立运行态“注册手机号”输入框,位于接码订单运行状态下方;第 2 步自动拿到号码后立即回填,用户手动接管手机号注册时也可以直接改写这一个运行态槽位。这个输入框表达账号身份,不等同于接码订单;后续自动拉短信仍依赖 `signupPhoneActivation / signupPhoneCompletedActivation`
@@ -196,7 +196,7 @@
- LuckMail API 配置
- 自动运行默认配置
- OAuth 授权后链总超时开关 `oauthFlowTimeoutEnabled`:默认开启;关闭后会立即清空已存在的 OAuth 总预算 deadline,仅保留各步骤本地等待超时
- 账号运行历史 `accountRunHistory`(以统一账号标识为主键邮箱账号保留邮箱主键phone-only 账号改存 `accountIdentifierType / accountIdentifier / phoneNumber`,保存最近一次状态:成功/失败/停止)
- 账号运行历史 `accountRunHistory`(以统一账号标识为主键邮箱账号保留邮箱主键,手机号注册保留手机号主键;同一轮中后续绑定的邮箱或手机号会写入同一条记录的副身份字段,保存最近一次状态:成功/失败/停止)
- 账号运行历史本地同步 helper 地址
注意:
@@ -523,7 +523,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
5. 仅当出现精确成功徽标,且该徽标不是红色/错误态、页面上也没有同时可见的失败提示时,才判定成功
6. 识别 `认证失败:*``认证失败: timeout of 30000ms exceeded``回调 URL 提交失败: oauth flow is not pending` 等失败提示并立即报错
7. 完成平台侧验证
8. 追加账号运行历史成功记录
8. 追加账号运行历史成功记录,并携带本轮邮箱/手机号组合身份
9. 做成功后的清理与标记
Codex2API 补充:
@@ -673,7 +673,7 @@ Plus 模式可见步骤:
补充:
- 本地 helper 除了收信与验证码读取,还提供邮箱记录 JSON 快照同步接口。
- 本地 helper 除了收信与验证码读取,还提供账号记录 JSON 快照同步接口。
- 账号运行历史快照会按默认本地 helper 地址自动尝试同步,不再要求用户手动打开独立开关,也不再绑定 Hotmail 的本地助手模式。
- sidepanel 中 Hotmail 账号池的新增表单默认收起,头部通过共享按钮切换“添加账号 / 取消添加”;表单显隐、按钮文案切换、清空与聚焦都复用 `sidepanel/account-pool-ui.js`,不在 Hotmail manager 内重复实现一套。
@@ -875,7 +875,7 @@ Hide My Email 获取与管理链路:
- 立即停止
- 当前轮重试
- 下一轮继续
7. 当前轮最终失败时,写入邮箱记录,并在自动重试链路中累计重试次数
7. 当前轮最终失败时,写入账号记录,并在自动重试链路中累计重试次数
- 手动停止与自动停止会写入“停止”状态(若后续同邮箱成功/失败,会被覆盖为最新状态)
8. 如果配置了线程间隔,则挂计时计划;计时计划会带上当前 `autoRunSessionId`
9. 旧 timer / 旧 alarm / 旧恢复入口只有在 session 仍有效时才允许恢复执行;Stop 会立即使当前 session 失效,防止“停止后旧倒计时又把流程重新拉起”
+10 -10
View File
@@ -34,8 +34,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/开发者的完整功能链路说明,用于快速理解系统整体运行过程。
@@ -43,7 +43,7 @@
## `background/`
- `background/account-run-history.js`:账号记录模块,负责以统一账号标识维护最新记录(优先邮箱,phone-only 时改用 `accountIdentifier / phoneNumber`),在步骤 2 设定身份后支持先写入“停止/未完成”占位状态统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,兼容 phone-only 记录空密码,并按默认本地 helper 地址自动尝试把完整快照同步到本地 helper;若 helper 未启动,则静默跳过。
- `background/account-run-history.js`:账号记录模块,负责以统一账号标识维护最新记录;邮箱注册以邮箱为主身份,手机号注册以手机号为主身份,同一轮后续绑定的手机号或邮箱会并入同一条记录并覆盖旧占位状态;模块统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,兼容 phone-only 与 email+phone 组合记录空密码,并按默认本地 helper 地址自动尝试把完整快照同步到本地 helper;若 helper 未启动,则静默跳过。
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 Plus 模式与 PayPal 配置、`gmailBaseEmail``mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失关键持久配置。
- `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。
@@ -54,7 +54,7 @@
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;`LOG / STEP_COMPLETE / STEP_ERROR` 会把消息里的真实 `step` 继续传给结构化日志,跳过登录验证码这类派生日志也按当前步骤 key 写入;当前额外接入 PayPal 账号池的新增与切换、2925 账号池的新增、导入、切换、登录、禁用与删除消息,以及 IP 代理的同步、切换、Change 与出口检测消息;当侧栏关闭 `oauthFlowTimeoutEnabled` 时,会立即清空已存在的 OAuth 总预算 deadline。
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
- `background/paypal-account-store.js`:PayPal 账号池持久化模块,负责保存账号列表、切换当前账号,并把当前选中账号同步回兼容字段 `paypalEmail / paypalPassword`
- `background/phone-verification-flow.js`:手机号验证码共享流程模块,负责复用 HeroSMS / 5sim / NexSMS 的取号、复用、轮询、重发、换号与收尾能力;既承接 OAuth 后置 `add-phone / phone-verification` 页面,也承接手机号注册 Step 4 和 Step 8 中真实出现的 `phone-verification` 页面,并保持 `signupPhone*` 运行态与 `currentPhoneActivation` 隔离;该流程由调用方传入当前可见步骤号,手机号日志不会再固定显示普通模式 Step 9。
- `background/phone-verification-flow.js`:手机号验证码共享流程模块,负责复用 HeroSMS / 5sim / NexSMS 的取号、复用、轮询、重发、换号与收尾能力;既承接 OAuth 后置 `add-phone / phone-verification` 页面,也承接手机号注册 Step 4 和 Step 8 中真实出现的 `phone-verification` 页面,并保持 `signupPhone*` 运行态与 `currentPhoneActivation` 隔离;后置 add-phone 成功后会把已绑定手机号写入运行态 `phoneNumber`,供账号记录并入同一轮;该流程由调用方传入当前可见步骤号,手机号日志不会再固定显示普通模式 Step 9。
- `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;内容脚本恢复等待日志支持 `logStep / logStepKey`,用于保持 Plus 复用步骤的日志标签正确;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
@@ -123,7 +123,7 @@
## `scripts/`
- `scripts/hotmail_helper.py`:本地 helper 服务,负责通过本地接口协助 Hotmail 获取邮件和验证码,并提供邮箱记录 JSON 快照同步接口;旧的文本追加接口仍保留作兼容。
- `scripts/hotmail_helper.py`:本地 helper 服务,负责通过本地接口协助 Hotmail 获取邮件和验证码,并提供账号记录 JSON 快照同步接口;旧的文本追加接口仍保留作兼容。
## `sidepanel/`
@@ -133,13 +133,13 @@
- `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
- `sidepanel/mail-2925-manager.js`:侧边栏 2925 账号池管理器,负责 2925 账号的新增、导入、切换、手动登录、启停、清冷却与删除。
- `sidepanel/form-dialog.js`:侧边栏公共表单弹窗模块,负责渲染可复用的小型表单弹窗,支持动态字段、校验、确认与取消。
- `sidepanel/account-records-manager.js`:侧边栏账号记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认;展示时优先显示邮箱,phone-only 记录则回退显示手机号或统一账号标识
- `sidepanel/account-records-manager.js`:侧边栏账号记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认;展示时`accountIdentifierType` 决定主身份,手机号注册显示手机号并把绑定邮箱放副行,邮箱注册显示邮箱并把后续绑定手机号放副行
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行(包括 Codex2API 配置)的禁用与隐藏。
- `sidepanel/ip-proxy-panel.js`:侧边栏 IP 代理面板逻辑,负责代理配置显隐、服务/模式切换、账号串参数同步、状态卡渲染,以及同步、下一条、Change、检测出口、检查 IP 等按钮行为。
- `sidepanel/ip-proxy-provider-711proxy.js`:侧边栏 711Proxy 输入辅助逻辑,负责从 host、username、region 输入中推断和归一化国家/地区参数。
- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行、统一的收起态列表高度样式,以及 IP 代理配置区、状态卡和操作按钮样式。
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接账号记录覆盖层,顶部新增“贡
献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内
展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“接码”开关与“验证码重发”拆成同一行展示;只有开启接码后,下面的 HeroSMS 平
台、国家与 API Key 行才会显示;页面继续加载 `managed-alias-utils.js``mail-provider-utils.js`,并把旧的“邮箱前缀”字段语义改为“别
@@ -150,7 +150,7 @@
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关与 PayPal 账号下拉框,右侧使用公共表单弹窗添加账号;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;设置卡片还新增“授权总超时”开关,用于控制 Step 7 后链 5 分钟总预算。
- `sidepanel/paypal-manager.js`:侧边栏 PayPal 账号管理器,负责 Plus 模式下的账号下拉框渲染、添加账号弹窗、保存账号与切换当前账号。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装
配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启
配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 账号记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启
动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接
到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显
示并同时服务于 provide / receive 两种模式;当 provider 为 iCloud 时,会保存并回显目标邮箱类型与转发邮箱 provider,相关 provider 选
@@ -177,7 +177,7 @@
- `tests/auto-run-step6-restart.test.js`:测试自动运行在后半段授权链路遇错时会回到步骤 7 重开,并在命中 add-phone 或泛化手机号页 fatal 错误时停止重开。
- `tests/auto-step-random-delay.test.js`:测试自动运行步间延迟与旧配置键兼容解析。
- `tests/background-auto-run-module.test.js`:测试自动运行控制器模块已接入且导出工厂。
- `tests/background-account-run-history-module.test.js`:测试邮箱记录模块已接入、导出工厂,能够保留并归一化停止记录、归一化失败标签、计算自动重试次数,并支持清理后同步完整快照。
- `tests/background-account-run-history-module.test.js`:测试账号记录模块已接入、导出工厂,能够保留并归一化停止记录、归一化失败标签、计算自动重试次数、归并同一轮邮箱/手机号身份,并支持清理后同步完整快照。
- `tests/background-account-history-settings.test.js`:测试账号运行历史的独立配置项归一化,不再复用 Hotmail 模式作为启停条件。
- `tests/background-contribution-mode.test.js`:测试贡献模式的后台运行态与公开 OAuth 接入,覆盖 `contributionMode` 只存在于运行态、`SET_CONTRIBUTION_MODE / POLL_CONTRIBUTION_STATUS` 消息接入、reset 保留贡献运行态,以及 callback 自动提交在“无需手动提交”场景下的兼容处理。
- `tests/background-custom-email-pool.test.js`:测试后台对自定义邮箱池生成方式,以及自定义邮箱服务号池的归一化、标签文案和按轮次取邮箱逻辑。
@@ -228,7 +228,7 @@
- `tests/microsoft-email.test.js`:测试 Microsoft 邮件拉取与验证码提取逻辑。
- `tests/sidepanel-hotmail-manager.test.js`:测试侧边栏 Hotmail 管理器模块接线、共享号池表单显隐交互与空态渲染。
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏邮箱记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏账号记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
- `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。
- `tests/sidepanel-custom-email-pool.test.js`:测试侧边栏自定义邮箱池、自定义邮箱服务号池的 HTML 接线,以及邮箱池长度与自动轮数之间的联动规则。
- `tests/sidepanel-contribution-mode.test.js`:测试侧边栏贡献模式的 HTML 接线、runtime-only 设置保护,以及贡献模式 manager 复用主自动流启动、状态轮询和退出清理逻辑。