fix: comprehensive extension context invalidated handling
Some checks failed
CI — Test & Build / Test & Build (push) Has been cancelled

This commit is contained in:
Forge
2026-03-06 13:56:30 +08:00
parent 4343abbab3
commit a0b00c6d87
3 changed files with 93 additions and 9 deletions

View File

@@ -204,7 +204,7 @@ export default defineContentScript({
function isExtensionValid(): boolean { function isExtensionValid(): boolean {
try { try {
return !!chrome.runtime?.id; return typeof chrome !== 'undefined' && !!chrome.runtime?.id;
} catch { } catch {
return false; return false;
} }
@@ -223,6 +223,25 @@ export default defineContentScript({
setTimeout(() => toast.remove(), 4000); setTimeout(() => toast.remove(), 4000);
} }
// ─── Safe chrome.runtime.sendMessage wrapper ──────────────────────────────
async function safeSendMessage(payload: Record<string, unknown>): Promise<unknown> {
if (!isExtensionValid()) {
showErrorToast('LexAI was updated — please refresh this page.');
return null;
}
try {
return await chrome.runtime.sendMessage(payload);
} catch (err) {
if (String(err).includes('Extension context invalidated') ||
String(err).includes('message channel closed')) {
showErrorToast('LexAI was updated — please refresh this page.');
hideToolbar();
}
return null;
}
}
// ─── LLM call ───────────────────────────────────────────────────────────── // ─── LLM call ─────────────────────────────────────────────────────────────
async function runAction(action: string) { async function runAction(action: string) {
@@ -251,13 +270,15 @@ export default defineContentScript({
} }
try { try {
const response = await chrome.runtime.sendMessage({ const response = await safeSendMessage({
type: 'ANALYZE_TEXT', type: 'ANALYZE_TEXT',
payload: { text: textToProcess, action }, payload: { text: textToProcess, action },
}); }) as { error?: string; result?: string } | null;
hideToolbar(); hideToolbar();
if (response === null) return; // safeSendMessage already handled the error
if (response?.error) { if (response?.error) {
showModal(`${response.error}`, null); showModal(`${response.error}`, null);
} else { } else {
@@ -265,8 +286,9 @@ export default defineContentScript({
} }
} catch (err) { } catch (err) {
hideToolbar(); hideToolbar();
if (String(err).includes('Extension context invalidated')) { if (String(err).includes('Extension context invalidated') ||
showErrorToast('LexAI was updated. Please refresh the page.'); String(err).includes('message channel closed')) {
showErrorToast('LexAI was updated — please refresh this page.');
} else { } else {
showModal(`❌ Error: ${String(err)}`, null); showModal(`❌ Error: ${String(err)}`, null);
} }
@@ -440,6 +462,25 @@ export default defineContentScript({
document.body.appendChild(modal); document.body.appendChild(modal);
} }
// ─── Global extension context error handlers ──────────────────────────────
window.addEventListener('error', (e) => {
if (e.message?.includes('Extension context invalidated')) {
e.preventDefault(); // suppress console error
hideToolbar();
showErrorToast('LexAI was updated — please refresh this page.');
}
});
window.addEventListener('unhandledrejection', (e) => {
if (String(e.reason)?.includes('Extension context invalidated') ||
String(e.reason)?.includes('message channel closed')) {
e.preventDefault();
hideToolbar();
showErrorToast('LexAI was updated — please refresh this page.');
}
});
// ─── Event listeners ────────────────────────────────────────────────────── // ─── Event listeners ──────────────────────────────────────────────────────
document.addEventListener('mouseup', (e) => { document.addEventListener('mouseup', (e) => {

View File

@@ -151,6 +151,30 @@ const styles = {
} as React.CSSProperties, } as React.CSSProperties,
}; };
// ─── Safe chrome storage wrappers ────────────────────────────────────────────
function safeStorageGet(keys: string[], callback: (result: Record<string, string>) => void) {
try {
if (typeof chrome === 'undefined' || !chrome.runtime?.id) return;
chrome.storage.local.get(keys, callback);
} catch (err) {
console.warn('LexAI: Extension context invalidated', err);
}
}
function safeStorageSet(data: Record<string, string>, callback?: () => void) {
try {
if (typeof chrome === 'undefined' || !chrome.runtime?.id) return;
if (callback) {
chrome.storage.local.set(data, callback);
} else {
chrome.storage.local.set(data);
}
} catch (err) {
console.warn('LexAI: Extension context invalidated', err);
}
}
// ─── Component ──────────────────────────────────────────────────────────────── // ─── Component ────────────────────────────────────────────────────────────────
function OptionsPage() { function OptionsPage() {
@@ -162,7 +186,7 @@ function OptionsPage() {
const apiKeyRef = useRef<HTMLInputElement>(null); const apiKeyRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
chrome.storage.local.get(["provider", "apiKey", "model"], (result: Record<string, string>) => { safeStorageGet(["provider", "apiKey", "model"], (result) => {
if (result.provider) setProvider(result.provider); if (result.provider) setProvider(result.provider);
if (result.apiKey) setApiKey(result.apiKey); if (result.apiKey) setApiKey(result.apiKey);
if (result.model) setModel(result.model); if (result.model) setModel(result.model);
@@ -183,7 +207,7 @@ function OptionsPage() {
return; return;
} }
setSaveStatus('saving'); setSaveStatus('saving');
chrome.storage.local.set({ provider, apiKey: apiKey.trim(), model }, () => { safeStorageSet({ provider, apiKey: apiKey.trim(), model }, () => {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
setSaveStatus('error'); setSaveStatus('error');
setTimeout(() => setSaveStatus('idle'), 3000); setTimeout(() => setSaveStatus('idle'), 3000);

View File

@@ -1,13 +1,24 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
// ─── Safe chrome storage wrapper ─────────────────────────────────────────────
function safeStorageGet(keys: string[], callback: (result: Record<string, string>) => void) {
try {
if (typeof chrome === 'undefined' || !chrome.runtime?.id) return;
chrome.storage.local.get(keys, callback);
} catch (err) {
console.warn('LexAI: Extension context invalidated', err);
}
}
function Popup() { function Popup() {
const [configured, setConfigured] = useState(false); const [configured, setConfigured] = useState(false);
const [provider, setProvider] = useState(''); const [provider, setProvider] = useState('');
const [model, setModel] = useState(''); const [model, setModel] = useState('');
useEffect(() => { useEffect(() => {
chrome.storage.local.get(["provider", "apiKey", "model"], (result: Record<string, string>) => { safeStorageGet(["provider", "apiKey", "model"], (result) => {
if (result.apiKey) { if (result.apiKey) {
setConfigured(true); setConfigured(true);
setProvider(result.provider || 'openai'); setProvider(result.provider || 'openai');
@@ -16,7 +27,15 @@ function Popup() {
}); });
}, []); }, []);
const openSettings = () => chrome.runtime.openOptionsPage(); const openSettings = () => {
try {
if (typeof chrome !== 'undefined' && chrome.runtime?.id) {
chrome.runtime.openOptionsPage();
}
} catch (err) {
console.warn('LexAI: Extension context invalidated', err);
}
};
return ( return (
<div style={{ padding: 16, fontFamily: 'system-ui, sans-serif', color: '#1e1e2e' }}> <div style={{ padding: 16, fontFamily: 'system-ui, sans-serif', color: '#1e1e2e' }}>