Fix startup crash and autosave input interruption
This commit is contained in:
@@ -9,14 +9,6 @@
|
||||
'auth0.openai.com',
|
||||
'accounts.openai.com',
|
||||
];
|
||||
const STEP1_COOKIE_CLEAR_ORIGINS = [
|
||||
'https://chatgpt.com',
|
||||
'https://chat.openai.com',
|
||||
'https://auth.openai.com',
|
||||
'https://auth0.openai.com',
|
||||
'https://accounts.openai.com',
|
||||
'https://openai.com',
|
||||
];
|
||||
|
||||
function normalizeCookieDomainForStep1(domain) {
|
||||
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
|
||||
@@ -30,6 +22,16 @@
|
||||
));
|
||||
}
|
||||
|
||||
function buildStep1CookieKey(cookie, fallbackStoreId = '') {
|
||||
return [
|
||||
cookie?.storeId || fallbackStoreId || '',
|
||||
cookie?.domain || '',
|
||||
cookie?.path || '',
|
||||
cookie?.name || '',
|
||||
cookie?.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
||||
].join('|');
|
||||
}
|
||||
|
||||
function buildStep1CookieRemovalUrl(cookie) {
|
||||
const host = normalizeCookieDomainForStep1(cookie?.domain);
|
||||
const rawPath = String(cookie?.path || '/');
|
||||
@@ -51,22 +53,39 @@
|
||||
: [{ id: undefined }];
|
||||
const cookies = [];
|
||||
const seen = new Set();
|
||||
const queryDomains = Array.from(
|
||||
new Set(
|
||||
STEP1_COOKIE_CLEAR_DOMAINS
|
||||
.map((domain) => normalizeCookieDomainForStep1(domain))
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
|
||||
for (const store of stores) {
|
||||
const storeId = store?.id;
|
||||
const batch = await chromeApi.cookies.getAll(storeId ? { storeId } : {});
|
||||
for (const cookie of batch || []) {
|
||||
if (!shouldClearStep1Cookie(cookie)) continue;
|
||||
const key = [
|
||||
cookie.storeId || storeId || '',
|
||||
cookie.domain || '',
|
||||
cookie.path || '',
|
||||
cookie.name || '',
|
||||
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
||||
].join('|');
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
cookies.push(cookie);
|
||||
for (const domain of queryDomains) {
|
||||
let batch = [];
|
||||
try {
|
||||
batch = await chromeApi.cookies.getAll(
|
||||
storeId
|
||||
? { storeId, domain }
|
||||
: { domain }
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[MultiPage:step1] query cookies failed', {
|
||||
storeId: storeId || '',
|
||||
domain,
|
||||
message: getStep1ErrorMessage(error),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const cookie of batch || []) {
|
||||
if (!shouldClearStep1Cookie(cookie)) continue;
|
||||
const key = buildStep1CookieKey(cookie, storeId);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
cookies.push(cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +131,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
await addLog('步骤 1:打开 ChatGPT 官网前清理 ChatGPT / OpenAI cookies...', 'info');
|
||||
const cookies = await collectStep1Cookies(chromeApi);
|
||||
let removedCount = 0;
|
||||
@@ -121,18 +141,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (chromeApi.browsingData?.removeCookies) {
|
||||
try {
|
||||
await chromeApi.browsingData.removeCookies({
|
||||
since: 0,
|
||||
origins: STEP1_COOKIE_CLEAR_ORIGINS,
|
||||
});
|
||||
} catch (error) {
|
||||
await addLog(`步骤 1:browsingData 补扫 cookies 失败:${getStep1ErrorMessage(error)}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
await addLog(`步骤 1:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies。`, 'ok');
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
await addLog(`步骤 1:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies(耗时 ${elapsedMs}ms)。`, 'ok');
|
||||
}
|
||||
|
||||
async function executeStep1() {
|
||||
|
||||
@@ -290,7 +290,7 @@
|
||||
}
|
||||
|
||||
async function closeConflictingTabsForSource(source, currentUrl, options = {}) {
|
||||
const { excludeTabIds = [] } = options;
|
||||
const { excludeTabIds = [], preserveActiveTab = true } = options;
|
||||
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
|
||||
const state = await getState();
|
||||
const lastUrl = getSourceMapValue(state.sourceLastUrls, source);
|
||||
@@ -299,8 +299,12 @@
|
||||
if (!referenceUrls.length) return;
|
||||
|
||||
const tabs = await queryTabsInAutomationWindow({});
|
||||
const activeTabId = preserveActiveTab
|
||||
? tabs.find((tab) => Boolean(tab?.active) && Number.isInteger(tab?.id))?.id || null
|
||||
: null;
|
||||
const matchedIds = tabs
|
||||
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
||||
.filter((tab) => !(preserveActiveTab && activeTabId !== null && tab.id === activeTabId))
|
||||
.filter((tab) => referenceUrls.some((refUrl) => matchesSourceUrlFamily(source, tab.url, refUrl)))
|
||||
.map((tab) => tab.id);
|
||||
|
||||
|
||||
+54
-5
@@ -8883,15 +8883,37 @@ function updateSaveButtonState() {
|
||||
btnSaveSettings.textContent = settingsSaveInFlight ? '保存中' : '保存';
|
||||
}
|
||||
|
||||
function isEditableElementInSettingsCard(element) {
|
||||
if (!element || !(element instanceof Element)) {
|
||||
return false;
|
||||
}
|
||||
const tagName = String(element.tagName || '').toLowerCase();
|
||||
const isEditableInput = (
|
||||
tagName === 'textarea'
|
||||
|| (tagName === 'input' && !['checkbox', 'radio', 'button', 'submit', 'reset', 'range', 'file', 'color'].includes(String(element.type || '').toLowerCase()))
|
||||
|| Boolean(element.isContentEditable)
|
||||
);
|
||||
if (!isEditableInput) {
|
||||
return false;
|
||||
}
|
||||
return !settingsCard || settingsCard.contains(element);
|
||||
}
|
||||
|
||||
function scheduleSettingsAutoSave() {
|
||||
clearTimeout(settingsAutoSaveTimer);
|
||||
settingsAutoSaveTimer = setTimeout(() => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
}, 500);
|
||||
const tryAutoSave = () => {
|
||||
const activeEl = typeof document !== 'undefined' ? document.activeElement : null;
|
||||
if (isEditableElementInSettingsCard(activeEl)) {
|
||||
settingsAutoSaveTimer = setTimeout(tryAutoSave, 800);
|
||||
return;
|
||||
}
|
||||
saveSettings({ silent: true, source: 'autosave' }).catch(() => { });
|
||||
};
|
||||
settingsAutoSaveTimer = setTimeout(tryAutoSave, 1200);
|
||||
}
|
||||
|
||||
async function saveSettings(options = {}) {
|
||||
const { silent = false, force = false } = options;
|
||||
const { silent = false, force = false, source = '' } = options;
|
||||
clearTimeout(settingsAutoSaveTimer);
|
||||
|
||||
if (!force && !settingsDirty && !settingsSaveInFlight && silent) {
|
||||
@@ -8903,6 +8925,14 @@ async function saveSettings(options = {}) {
|
||||
settingsSaveInFlight = true;
|
||||
updateSaveButtonState();
|
||||
|
||||
const shouldSkipStateApplyForFocusedEditor = (() => {
|
||||
if (!silent || source !== 'autosave') {
|
||||
return false;
|
||||
}
|
||||
const activeEl = typeof document !== 'undefined' ? document.activeElement : null;
|
||||
return isEditableElementInSettingsCard(activeEl);
|
||||
})();
|
||||
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'SAVE_SETTING',
|
||||
@@ -8915,7 +8945,12 @@ async function saveSettings(options = {}) {
|
||||
}
|
||||
|
||||
if (response?.state && saveRevision === settingsSaveRevision) {
|
||||
applySettingsState(response.state);
|
||||
if (shouldSkipStateApplyForFocusedEditor) {
|
||||
syncLatestState(response.state);
|
||||
markSettingsDirty(false);
|
||||
} else {
|
||||
applySettingsState(response.state);
|
||||
}
|
||||
} else {
|
||||
syncLatestState(payload);
|
||||
if (saveRevision === settingsSaveRevision) {
|
||||
@@ -14963,6 +14998,20 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
|
||||
case 'DATA_UPDATED': {
|
||||
syncLatestState(message.payload);
|
||||
const activeSettingsEditor = typeof document !== 'undefined' ? document.activeElement : null;
|
||||
const shouldDeferDataUpdatedUiApply = settingsSaveInFlight
|
||||
&& isEditableElementInSettingsCard(activeSettingsEditor);
|
||||
if (shouldDeferDataUpdatedUiApply) {
|
||||
// Avoid overwriting the focused editor while the current save request
|
||||
// is still in flight; otherwise typing can be interrupted by DATA_UPDATED.
|
||||
if (message.payload.operationDelayEnabled !== undefined && typeof applyOperationDelayState === 'function') {
|
||||
applyOperationDelayState(message.payload);
|
||||
}
|
||||
updateAccountRunHistorySettingsUI();
|
||||
renderContributionMode();
|
||||
void syncPlusManualConfirmationDialog();
|
||||
break;
|
||||
}
|
||||
if (message.payload.operationDelayEnabled !== undefined && typeof applyOperationDelayState === 'function') {
|
||||
applyOperationDelayState(message.payload);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadStep1Module() {
|
||||
const source = fs.readFileSync('background/steps/open-chatgpt.js', 'utf8');
|
||||
const globalScope = {};
|
||||
return new Function('self', `${source}; return self.MultiPageBackgroundStep1;`)(globalScope);
|
||||
}
|
||||
|
||||
test('step 1 cookie cleanup queries target domains and skips browsingData sweep when direct removals succeed', async () => {
|
||||
const api = loadStep1Module();
|
||||
const events = {
|
||||
getAllCalls: [],
|
||||
removedCookies: [],
|
||||
browsingDataCalls: [],
|
||||
openedSteps: [],
|
||||
completedNodes: [],
|
||||
};
|
||||
|
||||
const chromeApi = {
|
||||
cookies: {
|
||||
getAllCookieStores: async () => [{ id: 'store-a' }],
|
||||
getAll: async (query) => {
|
||||
events.getAllCalls.push(query);
|
||||
if (query?.domain === 'chatgpt.com') {
|
||||
return [
|
||||
{ domain: '.chatgpt.com', path: '/', name: 'session', storeId: 'store-a' },
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
remove: async (details) => {
|
||||
events.removedCookies.push(details);
|
||||
return details;
|
||||
},
|
||||
},
|
||||
browsingData: {
|
||||
removeCookies: async (details) => {
|
||||
events.browsingDataCalls.push(details);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const executor = api.createStep1Executor({
|
||||
addLog: async () => {},
|
||||
chrome: chromeApi,
|
||||
openSignupEntryTab: async (step) => {
|
||||
events.openedSteps.push(step);
|
||||
},
|
||||
completeNodeFromBackground: async (nodeId) => {
|
||||
events.completedNodes.push(nodeId);
|
||||
},
|
||||
});
|
||||
|
||||
await executor.executeStep1();
|
||||
|
||||
assert.ok(events.getAllCalls.length > 0, 'should query cookies at least once');
|
||||
assert.ok(events.getAllCalls.every((entry) => typeof entry?.domain === 'string' && entry.domain.length > 0));
|
||||
assert.deepStrictEqual(events.removedCookies, [
|
||||
{
|
||||
url: 'https://chatgpt.com/',
|
||||
name: 'session',
|
||||
storeId: 'store-a',
|
||||
},
|
||||
]);
|
||||
assert.deepStrictEqual(events.browsingDataCalls, []);
|
||||
assert.deepStrictEqual(events.openedSteps, [1]);
|
||||
assert.deepStrictEqual(events.completedNodes, ['open-chatgpt']);
|
||||
});
|
||||
|
||||
test('step 1 cookie cleanup skips browsingData sweep when no direct cookie is removed', async () => {
|
||||
const api = loadStep1Module();
|
||||
const events = {
|
||||
removedCookies: 0,
|
||||
browsingDataCalls: [],
|
||||
};
|
||||
|
||||
const chromeApi = {
|
||||
cookies: {
|
||||
getAllCookieStores: async () => [{ id: 'store-a' }],
|
||||
getAll: async () => [],
|
||||
remove: async () => {
|
||||
events.removedCookies += 1;
|
||||
return null;
|
||||
},
|
||||
},
|
||||
browsingData: {
|
||||
removeCookies: async (details) => {
|
||||
events.browsingDataCalls.push(details);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const executor = api.createStep1Executor({
|
||||
addLog: async () => {},
|
||||
chrome: chromeApi,
|
||||
openSignupEntryTab: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep1();
|
||||
|
||||
assert.equal(events.removedCookies, 0);
|
||||
assert.equal(events.browsingDataCalls.length, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user