docs: record phone auth country match test

This commit is contained in:
QLHazyCoder
2026-04-26 20:41:25 +08:00
9 changed files with 1154 additions and 142 deletions
+959 -137
View File
File diff suppressed because it is too large Load Diff
+10 -3
View File
@@ -7,10 +7,17 @@
root.IcloudUtils = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createIcloudUtils() {
function normalizeIcloudHost(rawHost) {
const host = String(rawHost || '').trim().toLowerCase();
let host = String(rawHost || '').trim().toLowerCase();
if (!host) return '';
if (host === 'icloud.com' || host === 'www.icloud.com' || host === 'setup.icloud.com') return 'icloud.com';
if (host === 'icloud.com.cn' || host === 'www.icloud.com.cn' || host === 'setup.icloud.com.cn') return 'icloud.com.cn';
try {
if (host.includes('://')) {
host = new URL(host).hostname.toLowerCase();
}
} catch {}
host = host.split('/')[0].split('?')[0].split('#')[0];
host = host.replace(/:\d+$/, '').replace(/\.$/, '');
if (host === 'icloud.com' || host.endsWith('.icloud.com')) return 'icloud.com';
if (host === 'icloud.com.cn' || host.endsWith('.icloud.com.cn')) return 'icloud.com.cn';
return '';
}
+21 -1
View File
@@ -388,6 +388,19 @@
}
}
function isLikelyIcloudLoginRequiredMessage(message = '') {
const lower = String(message || '').toLowerCase();
return lower.includes('请先在新打开的 icloud 页面中完成登录')
|| lower.includes('请先在当前浏览器登录')
|| lower.includes('需要先登录')
|| lower.includes('请先登录')
|| lower.includes('please sign in')
|| lower.includes('sign in required')
|| lower.includes('not logged in')
|| lower.includes('authentication required')
|| lower.includes('unauthenticated');
}
async function handleLoginDone() {
if (dom.btnIcloudLoginDone) {
dom.btnIcloudLoginDone.disabled = true;
@@ -405,7 +418,14 @@
helpers.showToast('iCloud 会话已恢复,别名列表已刷新。', 'success', 2600);
await refreshIcloudAliases({ silent: true });
} catch (err) {
helpers.showToast(`看起来还没有登录完成:${err.message}`, 'warn', 4200);
const errorMessage = String(err?.message || '未知错误');
if (isLikelyIcloudLoginRequiredMessage(errorMessage)) {
helpers.showToast(`看起来还没有登录完成:${errorMessage}`, 'warn', 4200);
return;
}
await refreshIcloudAliases({ silent: true }).catch(() => { });
helpers.showToast(`iCloud 会话校验失败(非登录态):${errorMessage}`, 'warn', 4200);
} finally {
if (dom.btnIcloudLoginDone) {
dom.btnIcloudLoginDone.disabled = false;
+109
View File
@@ -0,0 +1,109 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('icloud login helper distinguishes auth-required errors from transient context errors', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(
source,
/function isIcloudLoginRequiredError\(error\) \{[\s\S]*hasAuthStatus401[\s\S]*status \(409\|421\|429\|5\\d\\d\)[\s\S]*could not validate icloud session[\s\S]*return false;/m,
'login-required detection should only force login for auth failures and ignore transient 421/429/5xx statuses'
);
assert.match(
source,
/function isIcloudTransientContextError\(error\) \{[\s\S]*status \(401\|403\|409\|421\|429\|5\\d\\d\)[\s\S]*timeout[\s\S]*timed out/m,
'transient context detection should treat 421/429/5xx and timeout-like network errors as retryable context failures'
);
assert.match(
source,
/if \(isIcloudTransientContextError\(err\)\) \{[\s\S]*iCloud 别名加载受网络\/上下文波动影响,请稍后重试。/m,
'withIcloudLoginHelp should surface transient-context copy instead of forcing login prompt'
);
assert.match(
source,
/ICLOUD_TRANSIENT_RETRY_MAX_ATTEMPTS = 2/,
'icloud transient context handling should retry at least once before failing'
);
assert.match(
source,
/function getIcloudAliasCacheFromState\(state, options = \{\}\)/,
'icloud alias flow should expose cache lookup helper for transient fallback'
);
assert.match(
source,
/已回退最近缓存(\$\{[a-zA-Z0-9_]+\.length\} 条)/,
'icloud alias listing should fallback to cached aliases when transient context errors occur'
);
assert.match(
source,
/PERSISTENT_ALIAS_STATE_KEYS = \[[\s\S]*'icloudAliasCache'[\s\S]*'icloudAliasCacheAt'[\s\S]*\]/m,
'icloud alias cache should be persisted so transient fallback can survive restarts'
);
assert.match(
source,
/function shouldStopIcloudAutoFetchRetries\(error\) \{[\s\S]*网络\/上下文波动[\s\S]*status 421/m,
'icloud auto-fetch retry guard should stop repeated retries for transient session/context failures'
);
assert.match(
source,
/async function validateIcloudSessionViaPageContext\(tabId, setupUrl\) \{[\s\S]*world:\s*'MAIN'[\s\S]*\/validate/m,
'icloud service resolution should support page-context validate fallback when background validate keeps failing'
);
assert.match(
source,
/function isIcloudApiUrl\(url = ''\) \{[\s\S]*new URL\(rawUrl\)[\s\S]*hostname\.endsWith\('\.icloud\.com'\)[\s\S]*hostname\.endsWith\('\.icloud\.com\.cn'\)/m,
'icloud api url detection should match icloud subdomains so maildomainws hosts can trigger page-context fallback'
);
assert.match(
source,
/function appendIcloudClientQueryParams\(rawUrl = ''\) \{[\s\S]*clientBuildNumber[\s\S]*clientMasteringNumber[\s\S]*clientId[\s\S]*dsid/m,
'icloud maildomainws requests should include compatible client query params before sending requests'
);
assert.match(
source,
/function isIcloudMailPageUrl\(rawUrl = ''\) \{[\s\S]*pathname === '\/mail'[\s\S]*pathname\.startsWith\('\/mail\/'\)/m,
'icloud page-context fallback should be able to identify mail pages as preferred execution context'
);
assert.match(
source,
/async function waitForIcloudMailTabReady\(tabId, timeoutMs = 8000\) \{[\s\S]*status === 'complete'/m,
'icloud page-context fallback should wait for a created mail tab to finish loading before using it'
);
assert.match(
source,
/async function icloudRequestViaPageContext\(method, url, options = \{\}\) \{[\s\S]*ensureIcloudMailContextTab\([\s\S]*isIcloudMailPageUrl\(tab\?\.url\) \? 8 : 0[\s\S]*const mailTabs = sortedTabs\.filter/m,
'icloud page-context requests should ensure a mail-context tab exists and prioritize it before other icloud pages'
);
assert.match(
source,
/async function fetchIcloudHideMyEmail\(options = \{\}\) \{[\s\S]*const existingAliases = await listIcloudAliases\(\);/m,
'icloud auto-fetch should load aliases through listIcloudAliases so transient cache/local fallback applies before creation'
);
assert.match(
source,
/保留别名返回鉴权\/网络异常,正在回查别名列表确认是否已创建[\s\S]*保留请求异常,但已在列表确认别名/m,
'icloud auto-fetch should attempt list-confirmation recovery when reserve returns auth/network errors'
);
assert.match(
source,
/当前网络\/上下文波动,暂无法创建新别名,已临时回退复用/,
'icloud auto-fetch should fallback to reusable aliases when create-new fails due transient session/context issues'
);
});
+32 -1
View File
@@ -323,6 +323,18 @@ function getErrorMessage(error) {
function normalizeText(value) {
return String(value || '').replace(/\\s+/g, ' ').trim();
}
function appendIcloudClientQueryParams(url) {
return String(url || '');
}
function isIcloudMaildomainwsHost() {
return false;
}
function shouldTryIcloudRequestPageContextFallback() {
return false;
}
async function icloudRequestViaPageContext() {
throw new Error('not expected');
}
async function fetch() {
fetchCalls += 1;
if (fetchCalls === 1) {
@@ -381,6 +393,18 @@ function getErrorMessage(error) {
function normalizeText(value) {
return String(value || '').replace(/\\s+/g, ' ').trim();
}
function appendIcloudClientQueryParams(url) {
return String(url || '');
}
function isIcloudMaildomainwsHost() {
return false;
}
function shouldTryIcloudRequestPageContextFallback() {
return false;
}
async function icloudRequestViaPageContext() {
throw new Error('not expected');
}
async function fetch() {
fetchCalls += 1;
return {
@@ -451,6 +475,10 @@ async function loadNormalizedIcloudAliases() {
serviceUrl: 'https://p67-maildomainws.icloud.com',
};
}
async function listIcloudAliases() {
const response = await loadNormalizedIcloudAliases();
return response.aliases || [];
}
function pickReusableIcloudAlias() {
return null;
}
@@ -477,6 +505,9 @@ function broadcastIcloudAliasesChanged(payload) {
function findIcloudAliasByEmail(aliases, email) {
return (aliases || []).find((alias) => String(alias.email || '').toLowerCase() === String(email || '').toLowerCase()) || null;
}
function shouldStopIcloudAutoFetchRetries() {
return true;
}
${bundle}
return {
fetchIcloudHideMyEmail,
@@ -491,5 +522,5 @@ return {
assert.equal(email, 'fresh@icloud.com');
assert.deepEqual(api.readSelectedEmails(), ['fresh@icloud.com']);
assert.deepEqual(api.readBroadcasts(), [{ reason: 'created', email: 'fresh@icloud.com' }]);
assert.match(api.readLogs().map((entry) => entry.message).join('\n'), /按保留成功处理/);
assert.match(api.readLogs().map((entry) => entry.message).join('\n'), /已在列表确认别名/);
});
+2
View File
@@ -20,6 +20,8 @@ const {
test('normalizeIcloudHost and host preference helpers resolve supported hosts', () => {
assert.equal(normalizeIcloudHost('www.icloud.com'), 'icloud.com');
assert.equal(normalizeIcloudHost('setup.icloud.com.cn'), 'icloud.com.cn');
assert.equal(normalizeIcloudHost('setup.icloud.com:443'), 'icloud.com');
assert.equal(normalizeIcloudHost('https://setup.icloud.com.cn/setup/ws/1'), 'icloud.com.cn');
assert.equal(normalizeIcloudHost('example.com'), '');
assert.equal(getConfiguredIcloudHostPreference({ icloudHostPreference: 'icloud.com' }), 'icloud.com');
+12
View File
@@ -170,6 +170,7 @@
- Cloudflare / Temp Email 设置
- 接码开关,以及 HeroSMS 的 API Key 与默认国家设置
- iCloud 相关偏好:Host、获取策略、成功后自动删除、目标邮箱类型与转发收码邮箱 provider
- iCloud Hide My Email 别名缓存:`icloudAliasCache` / `icloudAliasCacheAt`,用于在 iCloud 会话或网络上下文短暂波动时回退展示最近可用列表
- LuckMail API 配置
- 自动运行默认配置
- 账号运行历史 `accountRunHistory`(以邮箱为主键,保存该邮箱最近一次状态:成功/失败/停止)
@@ -745,11 +746,22 @@ Plus 模式可见步骤:
- `icloudTargetMailboxType = icloud-inbox` 时,Step 4 / Step 8 直接打开 iCloud Mail 收件箱轮询验证码。
- `icloudTargetMailboxType = forward-mailbox` 时,注册邮箱仍由 iCloud Hide My Email 生成,但 Step 4 / Step 8 改为打开 `icloudForwardMailProvider` 指定的转发目标邮箱收码;当前支持 `qq / 163 / 163-vip / 126 / gmail`
Hide My Email 获取与管理链路:
1. 后台优先通过 `setup.icloud.com*/setup/ws/1/validate` 解析 `premiummailsettings.url`,并记录最近可用服务节点。
2. 如果后台请求受 401 / 403 / 409 / 421 / 429 / 5xx、CORS、超时或地址空间限制影响,会优先尝试在已打开或自动创建的 `www.icloud.com*/mail/` 页面主上下文中重放请求。
3. `maildomainws` 请求会补充 `clientBuildNumber / clientMasteringNumber / clientId / dsid` 查询参数,写请求使用 `text/plain;charset=UTF-8`,以兼容 iCloud 网页端接口。
4. 别名列表加载成功后会缓存到 `icloudAliasCache`;遇到短暂上下文波动时,列表展示会按“最近缓存 -> 历史缓存 -> 本地已用/保留记录”回退。
5. 生成新别名后若 `reserve` 返回鉴权或网络异常,会先回查列表确认候选别名是否已经创建;必要时刷新服务节点重试一次,最后才进入可复用别名回退或暂停等待用户处理。
6. 自动运行获取 iCloud 别名时,检测到会话或网络上下文异常后会停止重复消耗获取重试次数,转入等待邮箱状态,避免连续请求放大 iCloud 风控或上下文抖动。
维护约定:
- iCloud 转发目标邮箱 provider 的 label、URL、source、inject 配置统一由 `mail-provider-utils.js` 输出。
- `background.js` 只根据 `icloudTargetMailboxType` 选择 iCloud Mail 收件箱或共享转发邮箱配置,不再在主文件内硬编码各 provider 地址。
- `sidepanel/sidepanel.js` 只负责展示、保存和回显目标邮箱类型 / 转发邮箱 provider,不重复定义 provider 业务清单。
- iCloud 登录态提示必须区分“真实未登录”和“已登录但请求上下文波动”;不能把所有 401 / 403 / 421 都直接提示为未登录。
- iCloud 别名缓存只用于短暂失败回退,不应改变最终的已用 / 保留状态来源;状态仍以 `manualAliasUsage``preservedAliases` 与最新线上列表合并后的结果为准。
## 8. 自动运行完整链路
+7
View File
@@ -142,6 +142,13 @@
- 贡献模式的侧栏按钮、状态展示和轮询调度应优先收敛到独立 manager,例如 `sidepanel/contribution-mode.js`
- 如果服务端当前返回“无需手动提交 callback”,扩展端必须把它当兼容成功态处理,不能简单按 HTTP 非 200 直接视为失败
### 3.4.1 iCloud Hide My Email 维护补充
- iCloud Hide My Email 的会话校验、页面上下文回退、别名缓存回退和 `maildomainws` 兼容参数属于同一条链路;修改其中任一环时,必须同步检查列表加载、别名生成、保留、删除、自动运行等待邮箱和 sidepanel 登录提示。
- iCloud 登录态判断不能只看单个 HTTP 状态码。`401 / 403 / 421 / 429 / 5xx` 可能来自后台请求上下文、CORS 或服务节点波动,必须结合错误文案、页面上下文回退结果和用户提示文案判断。
- iCloud 别名缓存只能作为短暂失败回退,不允许替代线上列表成为最终状态来源;已用和保留状态仍然以 `manualAliasUsage``preservedAliases` 与最新线上列表合并后的结果为准。
- 如果新增 iCloud 相关回退路径,必须补覆盖登录提示、缓存回退、自动运行停止重试和 reserve 异常恢复的测试,并同步更新 [项目完整链路说明.md](c:/Users/projectf/Downloads/codex注册扩展/项目完整链路说明.md)。
## 4. 测试规范
### 4.1 原则
+2
View File
@@ -174,6 +174,7 @@
- `tests/background-custom-email-pool.test.js`:测试后台对自定义邮箱池生成方式,以及自定义邮箱服务号池的归一化、标签文案和按轮次取邮箱逻辑。
- `tests/background-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂,并覆盖 2925 receive 模式回退普通邮箱生成、自定义邮箱池按轮次取值、2925 provide 模式账号池预热,以及 iCloud `always_new` 传参。
- `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。
- `tests/background-icloud-login-help.test.js`:测试 iCloud 登录判定、页面上下文回退、别名缓存回退和保留异常恢复的关键源码约束。
- `tests/background-icloud-mail-provider.test.js`:测试 iCloud 邮箱 provider 配置解析,并覆盖转发收码模式复用共享 provider 配置。
- `tests/background-logging-status-module.test.js`:测试日志 / 状态模块已接入且导出工厂。
- `tests/background-luckmail.test.js`:测试 LuckMail 相关后台逻辑,如购买、复用、标记已用与重置。
@@ -194,6 +195,7 @@
- `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。
- `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成、等待标签稳定完成,以及等待过程中的 Stop 中断行为。
- `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。
- `tests/phone-auth-country-match.test.js`:测试认证页手机号国家选择在页面本地化时,仍能用 HeroSMS 的英文国家名匹配对应国家选项。
- `tests/phone-verification-flow.test.js`:测试手机号验证共享流程对 HeroSMS 的取号、复用、重发、换号与 Step 7 重开错误分支。
- `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe。
- `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑。