diff --git a/extension/background.js b/extension/background.js index c794baa..a882086 100644 --- a/extension/background.js +++ b/extension/background.js @@ -273,15 +273,6 @@ async function videoTabPayload(tabId, opened) { } } -async function openAiSidepanelForTab(tabId) { - if (chrome.sidePanel?.open) { - await chrome.sidePanel.open({ tabId }); - return; - } - - throw new Error("当前浏览器不支持扩展侧边栏"); -} - const FETCH_TIMEOUT_MS = 15000; // Obsidian's Local REST API and the cover images written into the vault; a hung request would block the write forever. @@ -496,7 +487,7 @@ const BACKGROUND_HANDLERS = { const tabId = Number(message.tabId || sender?.tab?.id || 0) || 0; if (!tabId) throw new Error(MdgErrors.TEXT.noTab); // Before any await: sidePanel.open needs the click that sent the message. - const openPromise = openAiSidepanelForTab(tabId); + const openPromise = MdgErrors.openSidepanel({ tabId }); // Awaited once settings load; without a handler now, a fast rejection is reported as uncaught. openPromise.catch(() => {}); const settings = await getMergedSettings(); diff --git a/extension/errors.js b/extension/errors.js index db260ef..378dd9f 100644 --- a/extension/errors.js +++ b/extension/errors.js @@ -76,6 +76,8 @@ netDown: "网络断了", // Asking Chrome or the background to open the side panel failed (popup, video page, 摘读记录, 分拣台); 「:原因」 follows. sidepanelOpenFailed: "打开侧边栏失败", + // The browser has no extension side panel, or turned it off (Chromium's "API Unavailable"): openSidepanel's reason. + sidepanelUnsupported: "当前浏览器不支持扩展侧边栏", // 继续问 on a 摘读记录 record that is neither a video nor a web page (a chrome:// or extension page). notVideo: "这个页面不能读取,不能继续问", // The side panel's 停止 ended the reply (the background's port message, the panel's own stop). @@ -369,6 +371,15 @@ return send(); } + // chrome.sidePanel.open for every entry (background, 摘读记录, 分拣台), called inside the click before any await. A + // browser without the API, or with it turned off (Chromium rejects with "API Unavailable"), says sidepanelUnsupported. + const openSidepanel = (options) => + chrome.sidePanel?.open + ? chrome.sidePanel.open(options).catch((error) => { + throw /API Unavailable/i.test(String(error?.message || "")) ? new Error(TEXT.sidepanelUnsupported, { cause: error }) : error; + }) + : Promise.reject(new Error(TEXT.sidepanelUnsupported)); + // What chrome.runtime calls throw in a content script whose extension was reloaded (badges.js checks // chrome.runtime.id instead). const reloaded = (error) => /Extension context invalidated/.test(String(error?.message || "")); @@ -394,5 +405,5 @@ else node.removeAttribute("aria-description"); } - globalThis.MdgErrors = Object.freeze({ TEXT, LABEL, action, remedies, pageCode, pageText, grantPages, subtitleCode, text, messageOf, fromResp, grantOrigin, noReceiver, pageSilent, tabMessage, reloaded, escapeHtml, setReason, subtitleMissing, aiTimeout, aiStalled, upTagTitle, noAccess, NET_TIMEOUT_MS, netTimeout, CODE, TIMEOUT_STATUS }); + globalThis.MdgErrors = Object.freeze({ TEXT, LABEL, action, remedies, pageCode, pageText, grantPages, subtitleCode, text, messageOf, fromResp, grantOrigin, noReceiver, pageSilent, tabMessage, openSidepanel, reloaded, escapeHtml, setReason, subtitleMissing, aiTimeout, aiStalled, upTagTitle, noAccess, NET_TIMEOUT_MS, netTimeout, CODE, TIMEOUT_STATUS }); })(); diff --git a/extension/errors.selftest.js b/extension/errors.selftest.js index 4a7aeeb..8d75fd2 100644 --- a/extension/errors.selftest.js +++ b/extension/errors.selftest.js @@ -83,6 +83,17 @@ assert.strictEqual(E.messageOf(null, "兜底"), "兜底"); for (const file of [...sources, "note.js"]) { assert.ok(!/拿不到字幕|字幕抓取失败[:(]\$\{|无字幕[,。]/.test(read(file)), `${file} words a missing subtitle itself; use MdgErrors.subtitleMissing`); } + // openSidepanel: Chromium's "API Unavailable" (side panel turned off) and a missing API say the same sentence; any other + // rejection passes on as it is. + const opens = async (open) => { + ctx.chrome.sidePanel = open && { open }; + return E.openSidepanel({ tabId: 1 }).then(() => "opened", (error) => error.message); + }; + assert.strictEqual(await opens(async () => {}), "opened"); + assert.strictEqual(await opens(async () => { throw new Error("API Unavailable"); }), E.TEXT.sidepanelUnsupported); + assert.strictEqual(await opens(null), E.TEXT.sidepanelUnsupported); + assert.strictEqual(await opens(async () => { throw new Error("No active side panel for tabId: 1"); }), "No active side panel for tabId: 1"); + console.log("errors selftest ok"); })().catch((e) => { console.error(e); diff --git a/extension/history/history.js b/extension/history/history.js index 57c9613..af93706 100644 --- a/extension/history/history.js +++ b/extension/history/history.js @@ -476,7 +476,7 @@ const readsFromPage = (g) => isWebPage(g) || Boolean(g.context.videoId && MdgSit // click (sidePanel.open needs the gesture), then its tab in this window is focused, or opened, and the panel asked for // the conversation as a video's is (askSidepanel). function askAboutPage(group) { - const opening = chrome.sidePanel.open({ windowId: OWN_TAB?.windowId }); + const opening = MdgErrors.openSidepanel({ windowId: OWN_TAB?.windowId }); setStatus(MdgErrors.TEXT.sidepanelOpening); void (async () => { try { @@ -782,7 +782,7 @@ els.list.addEventListener("click", (event) => { MdgLimits.askSidepanel({ tabId: OWN_TAB?.id, contextRef: group.context }); const opening = MdgErrors.TEXT.sidepanelOpening; setStatus(opening); - chrome.sidePanel.open({ tabId: OWN_TAB?.id }).then( + MdgErrors.openSidepanel({ tabId: OWN_TAB?.id }).then( () => els.status.textContent === opening && setStatus(""), (error) => setStatus(`${MdgErrors.TEXT.sidepanelOpenFailed}:${error.message}`, true) ); diff --git a/extension/triage/fav-base.js b/extension/triage/fav-base.js index 8c64bd8..09835fd 100644 --- a/extension/triage/fav-base.js +++ b/extension/triage/fav-base.js @@ -375,7 +375,7 @@ let ownTabId; function askAi(it) { const contextRef = { site: "bilibili", videoId: it.bvid, title: it.title, author: it.upper, url: videoUrl(it.bvid) }; MdgLimits.askSidepanel({ tabId: ownTabId, contextRef }); - chrome.sidePanel.open({ tabId: ownTabId }).catch((err) => toast(`${MdgErrors.TEXT.sidepanelOpenFailed}:${err.message}`, true)); + MdgErrors.openSidepanel({ tabId: ownTabId }).catch((err) => toast(`${MdgErrors.TEXT.sidepanelOpenFailed}:${err.message}`, true)); } // Busy button: disabled + aria-busy (tokens.css draws the spinner) + 「…中」 text; false restores it.