修复 Step 5 导航交棒后的完成收尾
This commit is contained in:
+109
-17
@@ -11028,38 +11028,98 @@ async function executeNodeViaCompletionSignal(nodeId, timeoutMs = 0) {
|
|||||||
error => ({ ok: false, error }),
|
error => ({ ok: false, error }),
|
||||||
);
|
);
|
||||||
|
|
||||||
let executeError = null;
|
const executeResultPromise = executeNode(normalizedNodeId, { deferRetryableTransportError: true }).then(
|
||||||
try {
|
() => ({ ok: true }),
|
||||||
await executeNode(normalizedNodeId, { deferRetryableTransportError: true });
|
error => ({ ok: false, error }),
|
||||||
} catch (err) {
|
);
|
||||||
executeError = err;
|
|
||||||
if (isStopError(err) || !isRetryableContentScriptTransportError(err)) {
|
|
||||||
notifyNodeError(normalizedNodeId, getErrorMessage(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const completionResult = await completionResultPromise;
|
let executeResult = null;
|
||||||
|
let completionResult = null;
|
||||||
|
const firstResult = await Promise.race([
|
||||||
|
executeResultPromise.then(result => ({ type: 'execute', result })),
|
||||||
|
completionResultPromise.then(result => ({ type: 'completion', result })),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (firstResult.type === 'completion') {
|
||||||
|
completionResult = firstResult.result;
|
||||||
if (completionResult.ok) {
|
if (completionResult.ok) {
|
||||||
if (executeError) {
|
executeResultPromise.then((result) => {
|
||||||
|
if (!result.ok) {
|
||||||
|
const error = result.error;
|
||||||
|
if (isStopError(error) || !isRetryableContentScriptTransportError(error)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
LOG_PREFIX,
|
LOG_PREFIX,
|
||||||
`[executeNodeViaCompletionSignal] node ${normalizedNodeId} completed after deferred execute error: ${getErrorMessage(executeError)}`
|
`[executeNodeViaCompletionSignal] node ${normalizedNodeId} completed before execute returned, later execute error: ${getErrorMessage(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
return completionResult.payload;
|
||||||
|
}
|
||||||
|
executeResultPromise.then((result) => {
|
||||||
|
if (!result.ok) {
|
||||||
|
const error = result.error;
|
||||||
|
if (isStopError(error) || !isRetryableContentScriptTransportError(error)) {
|
||||||
|
console.warn(
|
||||||
|
LOG_PREFIX,
|
||||||
|
`[executeNodeViaCompletionSignal] node ${normalizedNodeId} completion waiter failed before execute returned, later execute error: ${getErrorMessage(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
if (/等待超时/.test(getErrorMessage(completionResult.error)) && normalizedNodeId === 'fill-profile') {
|
||||||
|
const recoveredPayload = await completeStep5FromTabUrlAfterTransportError(completionResult.error).catch(() => null);
|
||||||
|
if (recoveredPayload) {
|
||||||
|
notifyNodeComplete(normalizedNodeId, recoveredPayload);
|
||||||
|
return recoveredPayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw completionResult.error;
|
||||||
|
} else {
|
||||||
|
executeResult = firstResult.result;
|
||||||
|
if (!executeResult.ok) {
|
||||||
|
const err = executeResult.error;
|
||||||
|
if (isStopError(err) || !isRetryableContentScriptTransportError(err)) {
|
||||||
|
notifyNodeError(normalizedNodeId, getErrorMessage(err));
|
||||||
|
} else if (normalizedNodeId === 'fill-profile') {
|
||||||
|
const recoveredPayload = await completeStep5FromTabUrlAfterTransportError(err).catch(() => null);
|
||||||
|
if (recoveredPayload) {
|
||||||
|
notifyNodeComplete(normalizedNodeId, recoveredPayload);
|
||||||
|
return recoveredPayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completionResult = await completionResultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completionResult.ok) {
|
||||||
|
if (executeResult && !executeResult.ok) {
|
||||||
|
console.warn(
|
||||||
|
LOG_PREFIX,
|
||||||
|
`[executeNodeViaCompletionSignal] node ${normalizedNodeId} completed after deferred execute error: ${getErrorMessage(executeResult.error)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return completionResult.payload;
|
return completionResult.payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (executeError && isRetryableContentScriptTransportError(executeError)) {
|
if (executeResult && !executeResult.ok && isRetryableContentScriptTransportError(executeResult.error)) {
|
||||||
const completionMessage = getErrorMessage(completionResult.error);
|
const completionMessage = getErrorMessage(completionResult.error);
|
||||||
if (/等待超时/.test(completionMessage)) {
|
if (/等待超时/.test(completionMessage)) {
|
||||||
await finalizeDeferredNodeExecutionError(normalizedNodeId, executeError);
|
if (normalizedNodeId === 'fill-profile') {
|
||||||
throw executeError;
|
const recoveredPayload = await completeStep5FromTabUrlAfterTransportError(executeResult.error).catch(() => null);
|
||||||
|
if (recoveredPayload) {
|
||||||
|
notifyNodeComplete(normalizedNodeId, recoveredPayload);
|
||||||
|
return recoveredPayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await finalizeDeferredNodeExecutionError(normalizedNodeId, executeResult.error);
|
||||||
|
throw executeResult.error;
|
||||||
}
|
}
|
||||||
throw completionResult.error;
|
throw completionResult.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (executeError) {
|
if (executeResult && !executeResult.ok) {
|
||||||
throw executeError;
|
throw executeResult.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw completionResult.error;
|
throw completionResult.error;
|
||||||
@@ -11074,6 +11134,38 @@ async function executeStepViaCompletionSignal(step, timeoutMs = 0) {
|
|||||||
return executeNodeViaCompletionSignal(nodeId, timeoutMs);
|
return executeNodeViaCompletionSignal(nodeId, timeoutMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function completeStep5FromTabUrlAfterTransportError(sourceError = null) {
|
||||||
|
const signupTabId = await getTabId('openai-auth').catch(() => null);
|
||||||
|
if (!Number.isInteger(signupTabId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab = await waitForTabStableComplete(signupTabId, {
|
||||||
|
timeoutMs: 30000,
|
||||||
|
retryDelayMs: 300,
|
||||||
|
stableMs: 1000,
|
||||||
|
initialDelayMs: 300,
|
||||||
|
}).catch(() => null);
|
||||||
|
const currentUrl = String(tab?.url || '').trim();
|
||||||
|
if (!currentUrl || !isStep5CompletionChatgptUrl(currentUrl)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
profileSubmitted: true,
|
||||||
|
postSubmitChecked: true,
|
||||||
|
outcome: 'logged_in_home',
|
||||||
|
url: currentUrl,
|
||||||
|
recoveredFromTransportError: true,
|
||||||
|
};
|
||||||
|
await addLog(
|
||||||
|
`步骤 5 [调试] 内容脚本通信中断,但后台确认标签页已进入 chatgpt.com,按提交成功收尾。原始错误:${getErrorMessage(sourceError)}`,
|
||||||
|
'warn',
|
||||||
|
{ step: 5, stepKey: 'fill-profile' }
|
||||||
|
);
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
function getLatestLogTimestamp(logs = [], fallback = 0) {
|
function getLatestLogTimestamp(logs = [], fallback = 0) {
|
||||||
if (!Array.isArray(logs) || !logs.length) {
|
if (!Array.isArray(logs) || !logs.length) {
|
||||||
return Number.isFinite(Number(fallback)) ? Number(fallback) : 0;
|
return Number.isFinite(Number(fallback)) ? Number(fallback) : 0;
|
||||||
|
|||||||
@@ -6528,18 +6528,27 @@ function getSerializableRect(el) {
|
|||||||
// Step 5: Fill Name & Birthday / Age
|
// Step 5: Fill Name & Birthday / Age
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function getStep5DirectCompletionPayload({ isAgeMode = false, navigationStarted = false, outcome = null } = {}) {
|
function getStep5DirectCompletionPayload({ isAgeMode = false, navigationStarted = false, navigationEventType = '', outcome = null } = {}) {
|
||||||
const payload = {
|
const payload = {
|
||||||
profileSubmitted: true,
|
profileSubmitted: true,
|
||||||
postSubmitChecked: true,
|
postSubmitChecked: !navigationStarted,
|
||||||
};
|
};
|
||||||
if (isAgeMode) {
|
if (isAgeMode) {
|
||||||
payload.ageMode = true;
|
payload.ageMode = true;
|
||||||
}
|
}
|
||||||
if (navigationStarted) {
|
if (navigationStarted) {
|
||||||
payload.navigationStarted = true;
|
payload.navigationStarted = true;
|
||||||
|
payload.handoffToBackground = true;
|
||||||
|
const resolvedNavigationEventType = String(navigationEventType || '').trim();
|
||||||
|
if (resolvedNavigationEventType) {
|
||||||
|
payload.navigationEventType = resolvedNavigationEventType;
|
||||||
|
}
|
||||||
|
if (typeof location !== 'undefined' && location?.href) {
|
||||||
|
payload.url = location.href;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (outcome?.state) {
|
if (outcome?.state) {
|
||||||
|
payload.postSubmitChecked = true;
|
||||||
payload.outcome = outcome.state;
|
payload.outcome = outcome.state;
|
||||||
}
|
}
|
||||||
if (outcome?.url) {
|
if (outcome?.url) {
|
||||||
@@ -6825,6 +6834,7 @@ function installStep5NavigationCompletionReporter(completeOnce) {
|
|||||||
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
|
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
let reportedNavigation = false;
|
||||||
const debugLog = typeof logStep5SubmitDebug === 'function'
|
const debugLog = typeof logStep5SubmitDebug === 'function'
|
||||||
? logStep5SubmitDebug
|
? logStep5SubmitDebug
|
||||||
: (message, options = {}) => {
|
: (message, options = {}) => {
|
||||||
@@ -6838,6 +6848,25 @@ function installStep5NavigationCompletionReporter(completeOnce) {
|
|||||||
|
|
||||||
const onNavigationStarted = (event) => {
|
const onNavigationStarted = (event) => {
|
||||||
const eventType = String(event?.type || 'navigation').trim() || 'navigation';
|
const eventType = String(event?.type || 'navigation').trim() || 'navigation';
|
||||||
|
if (reportedNavigation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reportedNavigation = true;
|
||||||
|
if (typeof completeOnce === 'function') {
|
||||||
|
try {
|
||||||
|
completeOnce({
|
||||||
|
navigationStarted: true,
|
||||||
|
navigationEventType: eventType,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof log === 'function') {
|
||||||
|
log(`步骤 5 [调试] 导航交棒信号发送失败:${error?.message || error}`, 'warn', {
|
||||||
|
step: 5,
|
||||||
|
stepKey: 'fill-profile',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
debugLog(`检测到页面开始导航(event=${eventType})。`, {
|
debugLog(`检测到页面开始导航(event=${eventType})。`, {
|
||||||
level: 'warn',
|
level: 'warn',
|
||||||
});
|
});
|
||||||
@@ -7240,13 +7269,18 @@ async function step5_fillNameBirthday(payload) {
|
|||||||
const completionPayload = getStep5DirectCompletionPayload({
|
const completionPayload = getStep5DirectCompletionPayload({
|
||||||
isAgeMode,
|
isAgeMode,
|
||||||
navigationStarted: Boolean(extra.navigationStarted),
|
navigationStarted: Boolean(extra.navigationStarted),
|
||||||
|
navigationEventType: extra.navigationEventType || '',
|
||||||
outcome: extra.outcome || null,
|
outcome: extra.outcome || null,
|
||||||
});
|
});
|
||||||
|
reportedCompletionPayload = completionPayload;
|
||||||
|
if (extra?.navigationStarted && typeof reportNodeComplete === 'function') {
|
||||||
|
reportNodeComplete('fill-profile', completionPayload);
|
||||||
|
} else {
|
||||||
|
reportComplete(5, completionPayload);
|
||||||
|
}
|
||||||
debugLog(`准备发送完成信号(reason=${completionReason},isAgeMode=${isAgeMode})。`, {
|
debugLog(`准备发送完成信号(reason=${completionReason},isAgeMode=${isAgeMode})。`, {
|
||||||
level: extra?.navigationStarted ? 'warn' : 'info',
|
level: extra?.navigationStarted ? 'warn' : 'info',
|
||||||
});
|
});
|
||||||
reportedCompletionPayload = completionPayload;
|
|
||||||
reportComplete(5, completionPayload);
|
|
||||||
return completionPayload;
|
return completionPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,3 +201,59 @@ return {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step 5 recovers from bfcache transport close when tab already reached chatgpt', async () => {
|
||||||
|
const api = new Function(`
|
||||||
|
const logs = [];
|
||||||
|
let waitCalls = 0;
|
||||||
|
|
||||||
|
async function getTabId(source) {
|
||||||
|
return source === 'openai-auth' ? 42 : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForTabStableComplete(tabId) {
|
||||||
|
waitCalls += 1;
|
||||||
|
if (tabId !== 42) throw new Error('unexpected tab id');
|
||||||
|
return { url: 'https://chatgpt.com/' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addLog(message, level, meta) {
|
||||||
|
logs.push({ message, level, meta });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(error) {
|
||||||
|
return String(error?.message || error || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
${extractFunction('parseUrlSafely')}
|
||||||
|
${extractFunction('isSignupEntryHost')}
|
||||||
|
${extractFunction('isLikelyLoggedInChatgptHomeUrl')}
|
||||||
|
${extractFunction('isStep5CompletionChatgptUrl')}
|
||||||
|
${extractFunction('completeStep5FromTabUrlAfterTransportError')}
|
||||||
|
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
return completeStep5FromTabUrlAfterTransportError(new Error('The page keeping the extension port is moved into back/forward cache, so the message channel is closed.'));
|
||||||
|
},
|
||||||
|
snapshot() {
|
||||||
|
return { logs, waitCalls };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const result = await api.run();
|
||||||
|
const snapshot = api.snapshot();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, {
|
||||||
|
profileSubmitted: true,
|
||||||
|
postSubmitChecked: true,
|
||||||
|
outcome: 'logged_in_home',
|
||||||
|
url: 'https://chatgpt.com/',
|
||||||
|
recoveredFromTransportError: true,
|
||||||
|
});
|
||||||
|
assert.equal(snapshot.waitCalls, 1);
|
||||||
|
assert.equal(
|
||||||
|
snapshot.logs.some(({ message }) => /通信中断,但后台确认标签页已进入 chatgpt\.com/.test(message)),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1109,7 +1109,7 @@ return {
|
|||||||
assert.equal(api.isCompletion('https://chatgpt.com/add-phone'), false);
|
assert.equal(api.isCompletion('https://chatgpt.com/add-phone'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('step 5 navigation reporter does not complete on beforeunload alone', () => {
|
test('step 5 navigation reporter hands off to background on beforeunload', () => {
|
||||||
const api = new Function(`
|
const api = new Function(`
|
||||||
const events = [];
|
const events = [];
|
||||||
const listeners = new Map();
|
const listeners = new Map();
|
||||||
@@ -1147,22 +1147,31 @@ ${extractFunction('installStep5NavigationCompletionReporter')}
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
run() {
|
run() {
|
||||||
let completionCount = 0;
|
const cleanup = installStep5NavigationCompletionReporter((payload) => {
|
||||||
const cleanup = installStep5NavigationCompletionReporter(() => {
|
events.push({ type: 'complete', payload });
|
||||||
completionCount += 1;
|
|
||||||
events.push({ type: 'complete' });
|
|
||||||
});
|
});
|
||||||
const beforeUnload = listeners.get('beforeunload');
|
const beforeUnload = listeners.get('beforeunload');
|
||||||
if (beforeUnload) {
|
if (beforeUnload) {
|
||||||
beforeUnload({ type: 'beforeunload' });
|
beforeUnload({ type: 'beforeunload' });
|
||||||
}
|
}
|
||||||
cleanup();
|
cleanup();
|
||||||
return { completionCount, events };
|
return { events };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
`)();
|
`)();
|
||||||
|
|
||||||
const result = api.run();
|
const result = api.run();
|
||||||
assert.equal(result.completionCount, 0);
|
assert.deepStrictEqual(
|
||||||
|
result.events.filter((entry) => entry.type === 'complete'),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'complete',
|
||||||
|
payload: {
|
||||||
|
navigationStarted: true,
|
||||||
|
navigationEventType: 'beforeunload',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
assert.equal(result.events.some((entry) => entry.type === 'log' && /检测到页面开始导航/.test(entry.message)), true);
|
assert.equal(result.events.some((entry) => entry.type === 'log' && /检测到页面开始导航/.test(entry.message)), true);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user