fix: stabilize iCloud login helper and verification polling

- 合并 PR #207 的核心改动:修复 iCloud 临时网络/上下文错误分支的 safeActionLabel TDZ 崩溃风险,并允许登录验证码轮询首次读取当前可见首行验证码。
- 本地补充修复:无,本轮仅完成审查和定向验证。
- 影响范围:background.js iCloud 登录辅助、content/icloud-mail.js 邮件轮询、对应 iCloud 回归测试。
This commit is contained in:
QLHazyCoder
2026-05-06 23:27:03 +08:00
committed by GitHub
4 changed files with 213 additions and 3 deletions
-1
View File
@@ -5485,7 +5485,6 @@ async function withIcloudLoginHelp(actionLabel, action) {
if (shouldEmitIcloudTransientLog(`${safeActionLabel}:final`)) {
await addLog(`iCloud${safeActionLabel}受网络/上下文波动影响:${getErrorMessage(err)}`, 'warn');
}
const safeActionLabel = String(actionLabel || '操作').trim() || '操作';
const transientError = new Error(`iCloud${safeActionLabel}受网络/上下文波动影响,请稍后重试。`);
transientError.code = 'ICLOUD_TRANSIENT_CONTEXT';
transientError.actionLabel = safeActionLabel;
+6 -2
View File
@@ -307,9 +307,13 @@ if (shouldHandlePollEmailInCurrentFrame) {
const items = collectThreadItems();
const useFallback = (fallbackCarry + attempt) > FALLBACK_AFTER;
for (const item of items) {
for (const [index, item] of items.entries()) {
const signature = buildItemSignature(item);
if (!useFallback && existingSignatures.has(signature)) {
const allowInitialStep8VisibleCode = Number(step) === 8
&& attempt === 1
&& index === 0
&& !sessionBaseline.fromCache;
if (!useFallback && existingSignatures.has(signature) && !allowInitialStep8VisibleCode) {
continue;
}
@@ -2,6 +2,47 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function extractFunctionBody(source, name) {
const start = source.indexOf(`function ${name}(`);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
for (let i = braceStart; i < source.length; i += 1) {
const ch = source[i];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
return source.slice(braceStart + 1, i);
}
}
}
throw new Error(`unterminated function ${name}`);
}
test('icloud login helper distinguishes auth-required errors from transient context errors', () => {
const source = fs.readFileSync('background.js', 'utf8');
@@ -107,3 +148,20 @@ test('icloud login helper distinguishes auth-required errors from transient cont
'icloud auto-fetch should fallback to reusable aliases when create-new fails due transient session/context issues'
);
});
test('icloud login helper does not redeclare safeActionLabel in transient branch', () => {
const source = fs.readFileSync('background.js', 'utf8');
const body = extractFunctionBody(source, 'withIcloudLoginHelp');
const declarations = body.match(/\bconst\s+safeActionLabel\b/g) || [];
assert.equal(
declarations.length,
1,
'withIcloudLoginHelp should declare safeActionLabel once to avoid temporal-dead-zone crashes'
);
assert.match(
body,
/const transientError = new Error\(`iCloud\$\{safeActionLabel\}受网络\/上下文波动影响,请稍后重试。`\);/,
'transient context errors should use the already-initialized safeActionLabel'
);
});
+149
View File
@@ -294,3 +294,152 @@ return {
assert.equal(result.code, '556677');
});
test('icloud step8 polling finds a visible first-row code immediately', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('getThreadItemMetadata'),
extractFunction('buildItemSignature'),
extractFunction('extractVerificationCode'),
extractFunction('normalizePollSessionKey'),
extractFunction('getOrCreatePollSessionBaseline'),
extractFunction('persistPollSessionBaseline'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
const ICLOUD_POLL_SESSION_CACHE = new Map();
function log() {}
function throwIfStopped() {}
async function sleep() {}
async function waitForElement() { return true; }
async function refreshInbox() { return true; }
const currentThreadData = [
{
signature: 'visible-code',
sender: 'noreply@tm.openai.com',
subject: '你的 OpenAI 代码为 576773',
preview: '输入此临时验证码以继续:576773',
timestamp: '21:05',
ariaLabel: 'visible-code',
},
{
signature: 'older-code',
sender: 'noreply@tm.openai.com',
subject: '你的 OpenAI 代码为 697852',
preview: '输入此临时验证码以继续:697852',
timestamp: '21:04',
ariaLabel: 'older-code',
},
];
function collectThreadItems() {
return currentThreadData.map((entry) => ({
getAttribute(name) {
if (name === 'aria-label') return entry.ariaLabel || entry.signature;
return '';
},
querySelector(selector) {
if (selector === '.thread-participants') return { textContent: entry.sender };
if (selector === '.thread-subject') return { textContent: entry.subject };
if (selector === '.thread-preview') return { textContent: entry.preview };
if (selector === '.thread-timestamp') return { textContent: entry.timestamp };
return null;
},
}));
}
async function openMailItemAndRead(item) {
const meta = getThreadItemMetadata(item);
return {
sender: meta.sender,
recipients: '',
timestamp: meta.timestamp,
bodyText: meta.preview,
combinedText: meta.combinedText,
};
}
${bundle}
return { handlePollEmail };
`)();
const result = await api.handlePollEmail(8, {
senderFilters: ['openai', 'noreply', 'chatgpt'],
subjectFilters: ['code', '验证码', 'login'],
maxAttempts: 1,
intervalMs: 10,
excludeCodes: [],
sessionKey: '8:visible-first',
});
assert.equal(result.code, '576773');
});
test('icloud step8 visible first-row code still respects excluded codes', async () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('getThreadItemMetadata'),
extractFunction('buildItemSignature'),
extractFunction('extractVerificationCode'),
extractFunction('normalizePollSessionKey'),
extractFunction('getOrCreatePollSessionBaseline'),
extractFunction('persistPollSessionBaseline'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
const ICLOUD_POLL_SESSION_CACHE = new Map();
function log() {}
function throwIfStopped() {}
async function sleep() {}
async function waitForElement() { return true; }
async function refreshInbox() { return true; }
const currentThreadData = [
{
signature: 'visible-code',
sender: 'noreply@tm.openai.com',
subject: '你的 OpenAI 代码为 576773',
preview: '输入此临时验证码以继续:576773',
timestamp: '21:05',
ariaLabel: 'visible-code',
},
];
function collectThreadItems() {
return currentThreadData.map((entry) => ({
getAttribute(name) {
if (name === 'aria-label') return entry.ariaLabel || entry.signature;
return '';
},
querySelector(selector) {
if (selector === '.thread-participants') return { textContent: entry.sender };
if (selector === '.thread-subject') return { textContent: entry.subject };
if (selector === '.thread-preview') return { textContent: entry.preview };
if (selector === '.thread-timestamp') return { textContent: entry.timestamp };
return null;
},
}));
}
async function openMailItemAndRead(item) {
const meta = getThreadItemMetadata(item);
return {
sender: meta.sender,
recipients: '',
timestamp: meta.timestamp,
bodyText: meta.preview,
combinedText: meta.combinedText,
};
}
${bundle}
return { handlePollEmail };
`)();
await assert.rejects(
() => api.handlePollEmail(8, {
senderFilters: ['openai', 'noreply', 'chatgpt'],
subjectFilters: ['code', '验证码', 'login'],
maxAttempts: 1,
intervalMs: 10,
excludeCodes: ['576773'],
sessionKey: '8:excluded-visible-first',
}),
/仍未在 iCloud 邮箱中找到新的匹配邮件/
);
});