From cdfb48f8667ac1ef8be1631f6e39c1fb1b6ccf0a Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:40:38 +0900 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E5=AE=8C=E6=95=B4=E7=9A=84permission?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=EF=BC=8C=E6=9B=B4=E5=A5=BD=E7=9A=84userScrip?= =?UTF-8?q?t=E6=9D=83=E9=99=90=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/runtime.ts | 50 +++++++---- src/pages/components/PopupWarnings/index.tsx | 66 +++++++------- src/pages/components/RuntimeSetting/index.tsx | 8 +- src/pages/install/App.tsx | 7 +- src/pkg/utils/utils.ts | 86 +++++++++++++++---- tests/pages/popup/App.test.tsx | 11 ++- 6 files changed, 147 insertions(+), 81 deletions(-) diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 0f2a2e00e..43b936534 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -23,6 +23,7 @@ import { obtainBlackList, sourceMapTo, } from "@App/pkg/utils/utils"; +import { BrowserType, getBrowserInstalledVersion, getBrowserType, isPermissionOk } from "@App/pkg/utils/utils"; import { cacheInstance } from "@App/app/cache"; import { UrlMatch } from "@App/pkg/utils/match"; import { ExtensionContentMessageSend } from "@Packages/message/extension_message"; @@ -162,22 +163,8 @@ export class RuntimeService { } } - showNoDeveloperModeWarning() { - // 判断是否首次 - this.localStorageDAO.get("firstShowDeveloperMode").then((res) => { - if (!res) { - this.localStorageDAO.save({ - key: "firstShowDeveloperMode", - value: true, - }); - // 打开页面 - initLocalesPromise.then(() => { - chrome.tabs.create({ - url: `${DocumentationSite}${localePath}/docs/use/open-dev/`, - }); - }); - } - }); + async showUserscriptActivationGuide() { + const storageKey = "firstShowDeveloperMode"; chrome.action.setBadgeBackgroundColor({ color: "#ff8c00", }); @@ -206,6 +193,35 @@ export class RuntimeService { }); } }); + + const currentInstalledBrowser = getBrowserInstalledVersion(); + const lastInstalledBrowser = (await this.localStorageDAO.get(storageKey))?.value as string | boolean | undefined; + // 判断是否安装后的首次,或是浏览器升级后的首次 + if (currentInstalledBrowser === lastInstalledBrowser) return; // 非首次则不弹出页面 + + const savePromise = this.localStorageDAO.save({ + key: storageKey, + value: currentInstalledBrowser, + }); + await Promise.allSettled([initLocalesPromise, this.initReady, savePromise]); // 等一下语言加载和 isUserScriptsAvailable 检查之类的 + + const userscript_enabled: boolean = this.isUserScriptsAvailable; + const permission = await isPermissionOk("userScripts"); + const browserType = getBrowserType(); + const guard = + browserType.chrome & BrowserType.guardedByDeveloperMode + ? "developerMode" + : browserType.chrome & BrowserType.guardedByAllowScript + ? "allowScript" + : "none"; + + // 打开页面 + const path = `${DocumentationSite}${localePath}/docs/use/open-dev/`; + let search = `?userscript_enabled=${userscript_enabled}&userscript_permission=${permission}&userscript_guard=${guard}`; + if (browserType.chrome & BrowserType.Edge) search += "&browser=edge"; + else if (browserType.chrome & BrowserType.Chrome) search += "&browser=chrome"; + const hash = `${guard === "developerMode" ? "#enable-developer-mode" : guard === "allowScript" ? "#allow-user-scripts" : ""}`; + chrome.tabs.create({ url: `${path}${search}${hash}` }); } async getInjectJsCode() { @@ -570,7 +586,7 @@ export class RuntimeService { // 检查是否开启了开发者模式 if (!this.isUserScriptsAvailable) { // 未开启加上警告引导 - this.showNoDeveloperModeWarning(); + this.showUserscriptActivationGuide(); let cid: ReturnType | number; cid = setInterval(async () => { if (!this.isUserScriptsAvailable) { diff --git a/src/pages/components/PopupWarnings/index.tsx b/src/pages/components/PopupWarnings/index.tsx index 066170d27..8ff441a57 100644 --- a/src/pages/components/PopupWarnings/index.tsx +++ b/src/pages/components/PopupWarnings/index.tsx @@ -1,7 +1,7 @@ import { Alert, Button } from "@arco-design/web-react"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { checkUserScriptsAvailable, getBrowserType, BrowserType } from "@App/pkg/utils/utils"; +import { checkUserScriptsAvailable, getBrowserType, BrowserType, isPermissionOk } from "@App/pkg/utils/utils"; import edgeMobileQrCode from "@App/assets/images/edge_mobile_qrcode.png"; interface PopupWarningsProps { @@ -39,16 +39,28 @@ function PopupWarnings({ isBlacklist }: PopupWarningsProps) { const browser = browserType.chrome & BrowserType.Edge ? "edge" : "chrome"; - const warningMessageHTML = browserType.firefox - ? t("develop_mode_guide", { browser: "firefox" }) - : browserType.chrome - ? browserType.chrome & BrowserType.chromeA + let warningMessageHTML; + + if (browserType.firefox) { + // firefox + warningMessageHTML = t("develop_mode_guide", { browser: "firefox" }); + } else if (browserType.chrome) { + // chrome + warningMessageHTML = + browserType.chrome & BrowserType.noUserScriptsAPI ? t("lower_version_browser_guide") - : (browserType.chrome & BrowserType.chromeC && browserType.chrome & BrowserType.Chrome) || - browserType.chrome & BrowserType.edgeA - ? t("allow_user_script_guide", { browser }) // Edge 144+ 后使用`允许用户脚本`控制 - : t("develop_mode_guide", { browser }) // Edge浏览器目前没有允许用户脚本选项,开启开发者模式即可 - : "UNKNOWN"; + : // 120+ + browserType.chrome & BrowserType.guardedByDeveloperMode + ? t("develop_mode_guide", { browser }) // Edge浏览器目前没有允许用户脚本选项,开启开发者模式即可 + : // Edge 144+ / Chrome 138+ + browserType.chrome & BrowserType.guardedByAllowScript + ? t("allow_user_script_guide", { browser }) // Edge 144+ 后使用`允许用户脚本`控制 + : // 用于日后扩充更新版本 + "UNKNOWN"; + } else { + // other browsers + warningMessageHTML = "UNKNOWN"; + } return warningMessageHTML; }, [isUserScriptsAvailableState, t]); @@ -62,28 +74,14 @@ function PopupWarnings({ isBlacklist }: PopupWarningsProps) { // 权限要求详见:https://github.com/mdn/webextensions-examples/blob/main/userScripts-mv3/options.mjs useEffect(() => { - //@ts-ignore - if (chrome.permissions?.contains && chrome.permissions?.request) { - chrome.permissions.contains( - { - permissions: ["userScripts"], - }, - function (permissionOK) { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error("chrome.runtime.lastError in chrome.permissions.contains:", lastError.message); - // runtime 错误的话不显示按钮 - return; - } - if (permissionOK === false) { - // 假设browser能支持 `chrome.permissions.contains` 及在 callback返回一个false值的话, - // chrome.permissions.request 应该可以执行 - // 因此在这裡显示按钮 - setShowRequestButton(true); - } - } - ); - } + isPermissionOk("userScripts").then((permissionOK) => { + if (permissionOK === false) { + // 假设browser能支持 `chrome.permissions.contains` 及在 callback返回一个false值的话, + // chrome.permissions.request 应该可以执行 + // 因此在这里显示按钮 + setShowRequestButton(true); + } + }); }, []); const handleRequestPermission = () => { @@ -103,8 +101,8 @@ function PopupWarnings({ isBlacklist }: PopupWarningsProps) { if (granted) { setPermissionReqResult("✅"); // UserScripts API相关的初始化: - // userScripts.LISTEN_CONNECTIONS 進行 Server 通讯初始化 - // onUserScriptAPIGrantAdded 進行 腳本注冊 + // userScripts.LISTEN_CONNECTIONS 进行 Server 通讯初始化 + // onUserScriptAPIGrantAdded 进行 脚本注册 updateIsUserScriptsAvailableState(); } else { setPermissionReqResult("❎"); diff --git a/src/pages/components/RuntimeSetting/index.tsx b/src/pages/components/RuntimeSetting/index.tsx index 60e7d2c7a..472d513c8 100644 --- a/src/pages/components/RuntimeSetting/index.tsx +++ b/src/pages/components/RuntimeSetting/index.tsx @@ -5,6 +5,7 @@ import FileSystemParams from "../FileSystemParams"; import { systemConfig } from "@App/pages/store/global"; import type { FileSystemType } from "@Packages/filesystem/factory"; import FileSystemFactory from "@Packages/filesystem/factory"; +import { isPermissionOk } from "@App/pkg/utils/utils"; const CollapseItem = Collapse.Item; @@ -24,11 +25,8 @@ const RuntimeSetting: React.FC = () => { setFilesystemType(res.filesystem); setFilesystemParam(res.params[res.filesystem] || {}); }); - chrome.permissions.contains({ permissions: ["background"] }, (result) => { - if (chrome.runtime.lastError) { - console.error(chrome.runtime.lastError); - return; - } + isPermissionOk("background").then((result) => { + if (result === null) return; // 无法要求 background permission setEnableBackgroundState(result); }); }, []); diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index d27481a9c..bf8ecac3d 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -31,7 +31,7 @@ import { intervalExecution, timeoutExecution } from "@App/pkg/utils/timer"; import { useSearchParams } from "react-router-dom"; import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key"; import { cacheInstance } from "@App/app/cache"; -import { formatBytes, prettyUrl } from "@App/pkg/utils/utils"; +import { formatBytes, isPermissionOk, prettyUrl } from "@App/pkg/utils/utils"; import { ScriptIcons } from "../options/routes/utils"; import { bytesDecode, detectEncoding } from "@App/pkg/utils/encoding"; @@ -462,9 +462,8 @@ function App() { if (hasShown !== "true") { // 检查是否已经有后台权限 - if (!(await chrome.permissions.contains({ permissions: ["background"] }))) { - return true; - } + const permission = await isPermissionOk("background"); + if (permission === false) return true; // optional permission "background" 需要显示后台运行提示 } return false; }; diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index 4ca023d9e..35cc3b93e 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -40,8 +40,8 @@ export const deferred = (): Deferred => { }; export function isFirefox() { - //@ts-ignore - return typeof mozInnerScreenX !== "undefined"; + // @ts-ignore. For both Page & Worker + return typeof mozInnerScreenX !== "undefined" || typeof navigator.mozGetUserMedia === "function"; } export function InfoNotification(title: string, msg: string) { @@ -256,17 +256,25 @@ export function getBrowserVersion(): number { // 判断是否为Edge浏览器 export function isEdge(): boolean { - return navigator.userAgent.includes("Edg/"); + return ( + // @ts-ignore; For Extension (Page/Worker), we can check UserSubscriptionState (hidden feature in Edge) + typeof chrome.runtime.UserSubscriptionState === "object" || + // Fallback to userAgent check + navigator.userAgent.includes("Edg/") + ); } -export enum BrowserType { - Edge = 2, - Chrome = 1, - chromeA = 4, // ~ 120 - chromeB = 8, // 121 ~ 137 - chromeC = 16, // 138 ~ - edgeA = 32, // Edge 144~ -} +export const BrowserType = { + Edge: 2, + Chrome: 1, + noUserScriptsAPI: 64, + guardedByDeveloperMode: 128, + guardedByAllowScript: 256, + Mouse: 1, // Desktop, Laptop. Tablet ?? + Touch: 2, // Touchscreen Laptop, Mobile, Tablet +} as const; + +export type BrowserType = ValueOf; export function getBrowserType() { const o = { @@ -275,25 +283,40 @@ export function getBrowserType() { chrome: 0, // Chrome, Chromium, Brave, Edge unknown: 0, chromeVersion: 0, + device: 0, }; if (isFirefox()) { + // Firefox, Zen o.firefox = 1; } else { //@ts-ignore const isWebkitBased = typeof webkitIndexedDB === "object"; if (isWebkitBased) { + // Safari, Orion o.webkit = 1; } else { - //@ts-ignore - const isChromeBased = typeof webkitRequestAnimationFrame === "function"; + const isChromeBased = + typeof requestAnimationFrame === "function" + ? // @ts-ignore. For Page only + typeof webkitRequestAnimationFrame === "function" + : // @ts-ignore. Available in Worker (Chrome 74+ Edge 79+) + typeof BackgroundFetchRecord === "function"; if (isChromeBased) { const isEdgeBrowser = isEdge(); const chromeVersion = getBrowserVersion(); o.chrome |= isEdgeBrowser ? BrowserType.Edge : BrowserType.Chrome; - o.chrome |= chromeVersion < 120 ? BrowserType.chromeA : 0; // Chrome 120 以下 - o.chrome |= chromeVersion < 138 ? BrowserType.chromeB : BrowserType.chromeC; // Chrome 121 ~ 137 / 138 以上 - if (isEdgeBrowser) { - o.chrome |= chromeVersion >= 144 ? BrowserType.edgeA : 0; // Edge 144 以上 + // 由小至大 + if (chromeVersion < 120) { + o.chrome |= BrowserType.noUserScriptsAPI; + } else { + // 120+ + if (isEdgeBrowser ? chromeVersion < 144 : chromeVersion < 138) { + o.chrome |= BrowserType.guardedByDeveloperMode; + } else { + // Edge 144+ / Chrome 138+ + o.chrome |= BrowserType.guardedByAllowScript; + // 如日后再变化,在这里再加条件式 + } } o.chromeVersion = chromeVersion; } else { @@ -301,9 +324,36 @@ export function getBrowserType() { } } } + // BrowserType.Mouse 未能在 Worker 使用 + o.device |= typeof matchMedia === "function" && !matchMedia("(hover: none)").matches ? BrowserType.Mouse : 0; + o.device |= navigator.maxTouchPoints > 0 ? BrowserType.Touch : 0; return o; } +export const isPermissionOk = async ( + manifestPermission: chrome.runtime.ManifestOptionalPermissions & chrome.runtime.ManifestPermissions +): Promise => { + // 兼容 Firefox - 避免因为检查 permission 时,该permission不存在于 optional permission 而报错 + const manifest = chrome.runtime.getManifest(); + if (manifest.optional_permissions?.includes(manifestPermission)) { + try { + return await chrome.permissions.contains({ permissions: [manifestPermission] }); + } catch { + // ignored + } + } else if (manifest.permissions?.includes(manifestPermission)) { + // mainfest 而列明有该permission, 不用检查 + return true; + } + return null; +}; + +export const getBrowserInstalledVersion = () => { + // unique for each browser update. + // Usage: Detect whether the browser is upgraded. + return btoa([...navigator.userAgent.matchAll(/[\d._]+/g)].map((e) => e[0]).join(";")); +}; + export const makeBlobURL = ( params: T, fallbackFn?: (params: T) => string | Promise @@ -513,7 +563,7 @@ export const normalizeResponseHeaders = (headersString: string) => { // 遵循 ISO 8601, 一月四日为Week 1,星期一为新一周 // 能应对每年开始和结束(不会因为踏入新一年而重新计算) // 见 https://wikipedia.org/wiki/ISO_week_date -// 中文說明 https://juejin.cn/post/6921245139855736846 +// 中文说明 https://juejin.cn/post/6921245139855736846 export const getISOWeek = (date: Date): number => { // 使用传入日期的年月日创建 UTC 日期对象,忽略本地时间部分,避免时区影响 const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); diff --git a/tests/pages/popup/App.test.tsx b/tests/pages/popup/App.test.tsx index fa1bde6e7..0d7f59d60 100644 --- a/tests/pages/popup/App.test.tsx +++ b/tests/pages/popup/App.test.tsx @@ -72,10 +72,15 @@ vi.mock("@App/pkg/utils/utils", () => ({ title: "Example", }), BrowserType: { - Chrome: "chrome", - Firefox: "firefox", - Edge: "edge", + Edge: 2, + Chrome: 1, + noUserScriptsAPI: 64, + guardedByDeveloperMode: 128, + guardedByAllowScript: 256, + Mouse: 1, + Touch: 2, }, + isPermissionOk: vi.fn(async (_s: string) => true), })); vi.mock("@App/locales/locales", () => ({