Fix startup crash and autosave input interruption
This commit is contained in:
@@ -9,14 +9,6 @@
|
|||||||
'auth0.openai.com',
|
'auth0.openai.com',
|
||||||
'accounts.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) {
|
function normalizeCookieDomainForStep1(domain) {
|
||||||
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
|
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) {
|
function buildStep1CookieRemovalUrl(cookie) {
|
||||||
const host = normalizeCookieDomainForStep1(cookie?.domain);
|
const host = normalizeCookieDomainForStep1(cookie?.domain);
|
||||||
const rawPath = String(cookie?.path || '/');
|
const rawPath = String(cookie?.path || '/');
|
||||||
@@ -51,22 +53,39 @@
|
|||||||
: [{ id: undefined }];
|
: [{ id: undefined }];
|
||||||
const cookies = [];
|
const cookies = [];
|
||||||
const seen = new Set();
|
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) {
|
for (const store of stores) {
|
||||||
const storeId = store?.id;
|
const storeId = store?.id;
|
||||||
const batch = await chromeApi.cookies.getAll(storeId ? { storeId } : {});
|
for (const domain of queryDomains) {
|
||||||
for (const cookie of batch || []) {
|
let batch = [];
|
||||||
if (!shouldClearStep1Cookie(cookie)) continue;
|
try {
|
||||||
const key = [
|
batch = await chromeApi.cookies.getAll(
|
||||||
cookie.storeId || storeId || '',
|
storeId
|
||||||
cookie.domain || '',
|
? { storeId, domain }
|
||||||
cookie.path || '',
|
: { domain }
|
||||||
cookie.name || '',
|
);
|
||||||
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
} catch (error) {
|
||||||
].join('|');
|
console.warn('[MultiPage:step1] query cookies failed', {
|
||||||
if (seen.has(key)) continue;
|
storeId: storeId || '',
|
||||||
seen.add(key);
|
domain,
|
||||||
cookies.push(cookie);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startedAt = Date.now();
|
||||||
await addLog('步骤 1:打开 ChatGPT 官网前清理 ChatGPT / OpenAI cookies...', 'info');
|
await addLog('步骤 1:打开 ChatGPT 官网前清理 ChatGPT / OpenAI cookies...', 'info');
|
||||||
const cookies = await collectStep1Cookies(chromeApi);
|
const cookies = await collectStep1Cookies(chromeApi);
|
||||||
let removedCount = 0;
|
let removedCount = 0;
|
||||||
@@ -121,18 +141,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chromeApi.browsingData?.removeCookies) {
|
const elapsedMs = Date.now() - startedAt;
|
||||||
try {
|
await addLog(`步骤 1:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies(耗时 ${elapsedMs}ms)。`, 'ok');
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeStep1() {
|
async function executeStep1() {
|
||||||
|
|||||||
@@ -290,7 +290,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function closeConflictingTabsForSource(source, currentUrl, options = {}) {
|
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 excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
const lastUrl = getSourceMapValue(state.sourceLastUrls, source);
|
const lastUrl = getSourceMapValue(state.sourceLastUrls, source);
|
||||||
@@ -299,8 +299,12 @@
|
|||||||
if (!referenceUrls.length) return;
|
if (!referenceUrls.length) return;
|
||||||
|
|
||||||
const tabs = await queryTabsInAutomationWindow({});
|
const tabs = await queryTabsInAutomationWindow({});
|
||||||
|
const activeTabId = preserveActiveTab
|
||||||
|
? tabs.find((tab) => Boolean(tab?.active) && Number.isInteger(tab?.id))?.id || null
|
||||||
|
: null;
|
||||||
const matchedIds = tabs
|
const matchedIds = tabs
|
||||||
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
.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)))
|
.filter((tab) => referenceUrls.some((refUrl) => matchesSourceUrlFamily(source, tab.url, refUrl)))
|
||||||
.map((tab) => tab.id);
|
.map((tab) => tab.id);
|
||||||
|
|
||||||
|
|||||||
+54
-5
@@ -8883,15 +8883,37 @@ function updateSaveButtonState() {
|
|||||||
btnSaveSettings.textContent = settingsSaveInFlight ? '保存中' : '保存';
|
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() {
|
function scheduleSettingsAutoSave() {
|
||||||
clearTimeout(settingsAutoSaveTimer);
|
clearTimeout(settingsAutoSaveTimer);
|
||||||
settingsAutoSaveTimer = setTimeout(() => {
|
const tryAutoSave = () => {
|
||||||
saveSettings({ silent: true }).catch(() => { });
|
const activeEl = typeof document !== 'undefined' ? document.activeElement : null;
|
||||||
}, 500);
|
if (isEditableElementInSettingsCard(activeEl)) {
|
||||||
|
settingsAutoSaveTimer = setTimeout(tryAutoSave, 800);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveSettings({ silent: true, source: 'autosave' }).catch(() => { });
|
||||||
|
};
|
||||||
|
settingsAutoSaveTimer = setTimeout(tryAutoSave, 1200);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSettings(options = {}) {
|
async function saveSettings(options = {}) {
|
||||||
const { silent = false, force = false } = options;
|
const { silent = false, force = false, source = '' } = options;
|
||||||
clearTimeout(settingsAutoSaveTimer);
|
clearTimeout(settingsAutoSaveTimer);
|
||||||
|
|
||||||
if (!force && !settingsDirty && !settingsSaveInFlight && silent) {
|
if (!force && !settingsDirty && !settingsSaveInFlight && silent) {
|
||||||
@@ -8903,6 +8925,14 @@ async function saveSettings(options = {}) {
|
|||||||
settingsSaveInFlight = true;
|
settingsSaveInFlight = true;
|
||||||
updateSaveButtonState();
|
updateSaveButtonState();
|
||||||
|
|
||||||
|
const shouldSkipStateApplyForFocusedEditor = (() => {
|
||||||
|
if (!silent || source !== 'autosave') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const activeEl = typeof document !== 'undefined' ? document.activeElement : null;
|
||||||
|
return isEditableElementInSettingsCard(activeEl);
|
||||||
|
})();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await chrome.runtime.sendMessage({
|
const response = await chrome.runtime.sendMessage({
|
||||||
type: 'SAVE_SETTING',
|
type: 'SAVE_SETTING',
|
||||||
@@ -8915,7 +8945,12 @@ async function saveSettings(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response?.state && saveRevision === settingsSaveRevision) {
|
if (response?.state && saveRevision === settingsSaveRevision) {
|
||||||
applySettingsState(response.state);
|
if (shouldSkipStateApplyForFocusedEditor) {
|
||||||
|
syncLatestState(response.state);
|
||||||
|
markSettingsDirty(false);
|
||||||
|
} else {
|
||||||
|
applySettingsState(response.state);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
syncLatestState(payload);
|
syncLatestState(payload);
|
||||||
if (saveRevision === settingsSaveRevision) {
|
if (saveRevision === settingsSaveRevision) {
|
||||||
@@ -14963,6 +14998,20 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|||||||
|
|
||||||
case 'DATA_UPDATED': {
|
case 'DATA_UPDATED': {
|
||||||
syncLatestState(message.payload);
|
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') {
|
if (message.payload.operationDelayEnabled !== undefined && typeof applyOperationDelayState === 'function') {
|
||||||
applyOperationDelayState(message.payload);
|
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