diff --git a/example/run-in/run-in_bg.js b/example/run-in/run-in_bg.js new file mode 100644 index 000000000..879d8f5e0 --- /dev/null +++ b/example/run-in/run-in_bg.js @@ -0,0 +1,19 @@ +// ==UserScript== +// @name Test @run-in background +// @namespace https://bbs.tampermonkey.net.cn/ +// @version 0.1.0 +// @description 后台脚本支持 @run-in 区分正常窗口与隐身窗口;同时可透过 GM_info.isIncognito 与 GM_info.userAgentData 取得运行环境 +// @author You +// @background +// @run-in incognito-tabs +// @grant GM_log +// ==/UserScript== + +return new Promise((resolve) => { + // 后台脚本指定 @run-in incognito-tabs 后,仅在隐身窗口对应的扩展环境中执行 + // 若改为 normal-tabs 则仅在正常窗口环境执行;不写或写 @run-in normal-tabs 与 @run-in incognito-tabs 时两者皆执行 + GM_log(`run-in: ${GM_info.script["run-in"]}`); + GM_log(`isIncognito: ${GM_info.isIncognito}`); + GM_log(`userAgentData: ${JSON.stringify(GM_info.userAgentData)}`); + resolve(); +}); diff --git a/packages/message/common.ts b/packages/message/common.ts index 91146211a..009fd8a51 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -1,3 +1,4 @@ +import { type TExtensionEnv } from "@App/app/service/extension/extension_env"; import { randomMessageFlag } from "@App/pkg/utils/utils"; // 避免页面载入后改动全域物件导致消息传递失败 @@ -21,7 +22,12 @@ export const pageDispatchCustomEvent = (eventType: string, detail: T) = }; // flag协商 -export function negotiateEventFlag(messageFlag: string, readyCount: number, onInit: (eventFlag: string) => void): void { +export function negotiateEventFlag( + messageFlag: string, + extensionEnv: TExtensionEnv, + readyCount: number, + onInit: (eventFlag: string) => void +): void { const eventFlag = randomMessageFlag(); onInit(eventFlag); // 监听 inject/content 发来的请求 eventFlag 的消息 @@ -40,27 +46,32 @@ export function negotiateEventFlag(messageFlag: string, readyCount: number, onIn break; case "requestEventFlag": // 广播通信 flag 给 inject/content - pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag }); + pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag, extensionEnv }); break; } }; // 设置事件,然后广播通信 flag 给 inject/content pageAddEventListener(messageFlag, fnEventFlagRequestHandler); - pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag }); + pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag, extensionEnv }); } // 获取协商后的 eventFlag -export function getEventFlag(messageFlag: string, onReady: (eventFlag: string) => void) { +export function getEventFlag( + messageFlag: string, + onReady: (eventFlag: string, extensionEnv: TExtensionEnv | undefined) => void +) { let eventFlag = ""; + let extensionEnv: TExtensionEnv | undefined = undefined; const fnEventFlagListener: EventListener = (ev: Event) => { if (!(ev instanceof CustomEvent)) return; if (ev.detail?.action != "broadcastEventFlag") return; eventFlag = ev.detail.eventFlag; + extensionEnv = ev.detail.extensionEnv; pageRemoveEventListener(messageFlag, fnEventFlagListener); // 告知对方已收到 eventFlag pageDispatchCustomEvent(messageFlag, { action: "receivedEventFlag" }); - onReady(eventFlag); + onReady(eventFlag, extensionEnv); }; // 设置事件,然后对 scripting 请求 flag diff --git a/src/app/service/content/exec_warp.test.ts b/src/app/service/content/exec_warp.test.ts new file mode 100644 index 000000000..83dfffb00 --- /dev/null +++ b/src/app/service/content/exec_warp.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest"; +import { BgExecScriptWarp } from "./exec_warp"; +import type { ScriptLoadInfo } from "../service_worker/types"; +import type { Message } from "@Packages/message/types"; +import type { TExtensionEnv } from "../extension/extension_env"; + +const buildScriptRes = () => + ({ + uuid: "uuid-bg", + name: "bg-script", + metadata: { + grant: ["none"], + version: ["1.0.0"], + }, + code: "return GM_info;", + sourceCode: "return GM_info;", + value: {}, + }) as unknown as ScriptLoadInfo; + +const fakeMessage = undefined as unknown as Message; + +const getGMInfo = (exec: BgExecScriptWarp) => (exec as any).named?.GM_info; + +describe("BgExecScriptWarp envInfo 注入", () => { + it("extensionEnv 为 undefined 时使用默认值", () => { + const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, undefined); + const gmInfo = getGMInfo(exec); + expect(gmInfo.isIncognito).toBe(false); + expect(gmInfo.userAgentData).toEqual({ + brands: [], + mobile: false, + platform: "", + }); + expect(gmInfo.sandboxMode).toBe("raw"); + }); + + it("inIncognitoContext=true 时覆盖 isIncognito 为 true", () => { + const extensionEnv: TExtensionEnv = { inIncognitoContext: true }; + const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, extensionEnv); + const gmInfo = getGMInfo(exec); + expect(gmInfo.isIncognito).toBe(true); + // 没传 userAgentData,沿用默认空值 + expect(gmInfo.userAgentData).toEqual({ + brands: [], + mobile: false, + platform: "", + }); + }); + + it("传入完整 userAgentData 时整体替换默认值", () => { + const extensionEnv: TExtensionEnv = { + inIncognitoContext: false, + userAgentData: { + brands: [{ brand: "Chromium", version: "120" }], + mobile: true, + platform: "Android", + architecture: "arm", + bitness: "64", + } as any, + }; + const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, extensionEnv); + const gmInfo = getGMInfo(exec); + expect(gmInfo.isIncognito).toBe(false); + expect(gmInfo.userAgentData).toEqual({ + brands: [{ brand: "Chromium", version: "120" }], + mobile: true, + platform: "Android", + architecture: "arm", + bitness: "64", + }); + }); + + it("userAgentData 为 null 时保留默认值", () => { + const extensionEnv: TExtensionEnv = { + inIncognitoContext: true, + userAgentData: null, + }; + const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, extensionEnv); + const gmInfo = getGMInfo(exec); + expect(gmInfo.isIncognito).toBe(true); + expect(gmInfo.userAgentData).toEqual({ + brands: [], + mobile: false, + platform: "", + }); + }); +}); diff --git a/src/app/service/content/exec_warp.ts b/src/app/service/content/exec_warp.ts index a965e91bc..4363d6803 100644 --- a/src/app/service/content/exec_warp.ts +++ b/src/app/service/content/exec_warp.ts @@ -2,6 +2,7 @@ import ExecScript from "./exec_script"; import type { Message } from "@Packages/message/types"; import type { ScriptLoadInfo } from "../service_worker/types"; import type { GMInfoEnv } from "./types"; +import { type TExtensionEnv } from "../extension/extension_env"; export class CATRetryError { msg: string; @@ -23,7 +24,7 @@ export class BgExecScriptWarp extends ExecScript { setInterval: Map; - constructor(scriptRes: ScriptLoadInfo, message: Message) { + constructor(scriptRes: ScriptLoadInfo, message: Message, extensionEnv: TExtensionEnv | undefined) { const thisContext: { [key: string]: any } = {}; const setTimeout = new Map(); const setInterval = new Map(); @@ -73,6 +74,9 @@ export class BgExecScriptWarp extends ExecScript { }, isIncognito: false, }; + const { inIncognitoContext, userAgentData } = extensionEnv || {}; + if (typeof inIncognitoContext === "boolean") envInfo.isIncognito = inIncognitoContext; + if (userAgentData) envInfo.userAgentData = userAgentData; super(scriptRes, { envPrefix: "offscreen", message: message, diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index 1d4715b6d..720c420d4 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -17,12 +17,12 @@ export type ExecScriptEntry = { scriptFunc: any; }; -export const initEnvInfo = { +export const initEnvInfo: GMInfoEnv = { /** userAgentData - 从全局变量获取 */ userAgentData: typeof UserAgentData === "object" ? UserAgentData : {}, /** sandboxMode - 预留字段,当前固定为 raw */ sandboxMode: "raw", - /** isIncognito - inject/content 环境下无法判断,固定为 false */ + /** isIncognito - inject/content 环境下透過 scripting 环境判断 */ /** 使用者可透过 「 await navigator.storage.persisted() 」来判断,但ScriptCat不会主动执行此代码来判断 */ isIncognito: false, } satisfies GMInfoEnv; diff --git a/src/app/service/content/script_runtime.ts b/src/app/service/content/script_runtime.ts index b93aa515d..817dd2a23 100644 --- a/src/app/service/content/script_runtime.ts +++ b/src/app/service/content/script_runtime.ts @@ -7,13 +7,15 @@ import type { GMInfoEnv, ValueUpdateDataEncoded } from "./types"; import type { ScriptEnvTag } from "@Packages/message/consts"; import { onInjectPageLoaded } from "./external"; import type { CustomEventMessage } from "@Packages/message/custom_event_message"; +import { type TExtensionEnv } from "../extension/extension_env"; export class ScriptRuntime { constructor( private readonly scripEnvTag: ScriptEnvTag, private readonly server: Server, private readonly msg: Message, - private readonly scriptExecutor: ScriptExecutor + private readonly scriptExecutor: ScriptExecutor, + private readonly extensionEnv: TExtensionEnv | undefined ) {} // content环境的特殊初始化 @@ -66,8 +68,13 @@ export class ScriptRuntime { this.startScripts(data.scripts, data.envInfo); }); + // 用于 early-start 的扩充参数 + const { inIncognitoContext } = this.extensionEnv || {}; + const initialEnvInfo = { ...initEnvInfo }; + if (typeof inIncognitoContext === "boolean") initialEnvInfo.isIncognito = inIncognitoContext; + // 检查early-start的脚本 - this.scriptExecutor.checkEarlyStartScript(this.scripEnvTag, initEnvInfo); + this.scriptExecutor.checkEarlyStartScript(this.scripEnvTag, initialEnvInfo); } startScripts(scripts: TScriptInfo[], envInfo: GMInfoEnv) { diff --git a/src/app/service/extension/extension_env.test.ts b/src/app/service/extension/extension_env.test.ts new file mode 100644 index 000000000..150475902 --- /dev/null +++ b/src/app/service/extension/extension_env.test.ts @@ -0,0 +1,113 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { extensionEnv, getExtensionUserAgentData } from "./extension_env"; + +describe("extensionEnv 常量", () => { + it("从 chrome.extension.inIncognitoContext 读取 incognito 状态", () => { + // mock 默认 inIncognitoContext = false + expect(extensionEnv.inIncognitoContext).toBe(false); + // userAgentData 为可选字段,常量初始化时不应填充 + expect(extensionEnv.userAgentData).toBeUndefined(); + }); +}); + +describe("getExtensionUserAgentData", () => { + const originalUserAgentData = (navigator as any).userAgentData; + const originalGetPlatformInfo = chrome.runtime.getPlatformInfo; + + const setNavigatorUserAgentData = (value: any) => { + Object.defineProperty(navigator, "userAgentData", { + configurable: true, + get: () => value, + }); + }; + + afterEach(() => { + setNavigatorUserAgentData(originalUserAgentData); + if (originalGetPlatformInfo === undefined) { + // @ts-ignore + delete chrome.runtime.getPlatformInfo; + } else { + chrome.runtime.getPlatformInfo = originalGetPlatformInfo; + } + vi.restoreAllMocks(); + }); + + it("navigator.userAgentData 缺失时返回 null", async () => { + setNavigatorUserAgentData(undefined); + const result = await getExtensionUserAgentData(); + expect(result).toBeNull(); + }); + + it("没有 chrome.runtime.getPlatformInfo 时只返回基础字段", async () => { + setNavigatorUserAgentData({ + brands: [{ brand: "Chromium", version: "120" }], + mobile: false, + platform: "macOS", + }); + // @ts-ignore + delete chrome.runtime.getPlatformInfo; + + const result = await getExtensionUserAgentData(); + expect(result).toEqual({ + brands: [{ brand: "Chromium", version: "120" }], + mobile: false, + platform: "macOS", + }); + expect((result as any).architecture).toBeUndefined(); + expect((result as any).bitness).toBeUndefined(); + }); + + it("getPlatformInfo 返回 x86-64 时 bitness 为 64", async () => { + setNavigatorUserAgentData({ + brands: [], + mobile: false, + platform: "Linux", + }); + chrome.runtime.getPlatformInfo = vi.fn().mockResolvedValue({ + os: "linux", + arch: "x86-64", + nacl_arch: "x86-64", + }) as any; + + const result = await getExtensionUserAgentData(); + expect(result?.architecture).toBe("x86-64"); + expect(result?.bitness).toBe("64"); + }); + + it("getPlatformInfo 返回 x86-32 时 bitness 为 32", async () => { + setNavigatorUserAgentData({ + brands: [], + mobile: false, + platform: "Windows", + }); + chrome.runtime.getPlatformInfo = vi.fn().mockResolvedValue({ + os: "win", + arch: "x86-32", + nacl_arch: "x86-32", + }) as any; + + const result = await getExtensionUserAgentData(); + expect(result?.architecture).toBe("x86-32"); + expect(result?.bitness).toBe("32"); + }); + + it("getPlatformInfo 抛异常时降级返回基础字段", async () => { + setNavigatorUserAgentData({ + brands: [{ brand: "Chromium", version: "120" }], + mobile: false, + platform: "Android", + }); + chrome.runtime.getPlatformInfo = vi.fn().mockRejectedValue(new Error("API not available")) as any; + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const result = await getExtensionUserAgentData(); + expect(result).toEqual({ + brands: [{ brand: "Chromium", version: "120" }], + mobile: false, + platform: "Android", + }); + expect((result as any).architecture).toBeUndefined(); + expect((result as any).bitness).toBeUndefined(); + expect(warnSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/app/service/extension/extension_env.ts b/src/app/service/extension/extension_env.ts new file mode 100644 index 000000000..e2108580c --- /dev/null +++ b/src/app/service/extension/extension_env.ts @@ -0,0 +1,35 @@ +export type TExtensionEnv = { + inIncognitoContext: boolean; + userAgentData?: GMUserAgentData | null; +}; + +type GMUserAgentData = typeof GM_info.userAgentData; + +export const extensionEnv: TExtensionEnv = { + inIncognitoContext: chrome.extension.inIncognitoContext, +} satisfies TExtensionEnv; + +export const getExtensionUserAgentData = async (): Promise => { + // @ts-ignore + const userAgentData = navigator.userAgentData; + if (userAgentData) { + const resultData: GMUserAgentData = { + brands: userAgentData.brands, + mobile: userAgentData.mobile, + platform: userAgentData.platform, + } satisfies GMUserAgentData; + // 处理architecture和bitness + if (chrome.runtime.getPlatformInfo) { + try { + const platformInfo = await chrome.runtime.getPlatformInfo(); + resultData.architecture = platformInfo.nacl_arch; + resultData.bitness = platformInfo.arch.includes("64") ? "64" : "32"; + } catch (e) { + // 避免 API 无法执行的问题。不影响整体运作 + console.warn(e); + } + } + return resultData; + } + return null; +}; diff --git a/src/app/service/offscreen/client.ts b/src/app/service/offscreen/client.ts index d168f6bcb..edd3ec724 100644 --- a/src/app/service/offscreen/client.ts +++ b/src/app/service/offscreen/client.ts @@ -8,6 +8,10 @@ export function preparationSandbox(windowMessage: WindowMessage) { return sendMessage(windowMessage, "offscreen/preparationSandbox"); } +export function getExtensionEnv(windowMessage: WindowMessage) { + return sendMessage(windowMessage, "offscreen/getExtensionEnv", { requireUAD: true }); +} + // 代理发送消息到ServiceWorker export function sendMessageToServiceWorker(windowMessage: WindowMessage, action: string, data?: any) { return sendMessage(windowMessage, "offscreen/sendMessageToServiceWorker", { action, data }); diff --git a/src/app/service/offscreen/index.ts b/src/app/service/offscreen/index.ts index 9dde44955..99464b282 100644 --- a/src/app/service/offscreen/index.ts +++ b/src/app/service/offscreen/index.ts @@ -35,6 +35,13 @@ export class OffscreenManager { sendMessage(this.msgSender, "serviceWorker/preparationOffscreen"); } + async getExtensionEnv(data: { requireUAD: boolean }) { + return this.sendMessageToServiceWorker({ + action: "getExtensionEnv", + data: data, + }); + } + sendMessageToServiceWorker(data: { action: string; data: any }) { return sendMessage(this.msgSender, `serviceWorker/${data.action}`, data.data); } @@ -43,6 +50,7 @@ export class OffscreenManager { // 监听消息 this.windowServer.on("logger", this.logger.bind(this)); this.windowServer.on("preparationSandbox", this.preparationSandbox.bind(this)); + this.windowServer.on("getExtensionEnv", this.getExtensionEnv.bind(this)); this.windowServer.on("sendMessageToServiceWorker", this.sendMessageToServiceWorker.bind(this)); const script = new ScriptService( this.windowServer.group("script"), diff --git a/src/app/service/sandbox/index.ts b/src/app/service/sandbox/index.ts index 817202747..7c2bfd9e3 100644 --- a/src/app/service/sandbox/index.ts +++ b/src/app/service/sandbox/index.ts @@ -1,6 +1,6 @@ import { Server } from "@Packages/message/server"; import { type WindowMessage } from "@Packages/message/window_message"; -import { preparationSandbox } from "../offscreen/client"; +import { getExtensionEnv, preparationSandbox } from "../offscreen/client"; import { Runtime } from "./runtime"; // sandbox环境的管理器 @@ -12,7 +12,8 @@ export class SandboxManager { } initManager() { - const runtime = new Runtime(this.windowMessage, this.api); + const extensionEnvAsync = getExtensionEnv(this.windowMessage); + const runtime = new Runtime(this.windowMessage, this.api, extensionEnvAsync); runtime.init(); // 通知初始化好环境了 preparationSandbox(this.windowMessage); diff --git a/src/app/service/sandbox/runtime.test.ts b/src/app/service/sandbox/runtime.test.ts new file mode 100644 index 000000000..6e9c36a15 --- /dev/null +++ b/src/app/service/sandbox/runtime.test.ts @@ -0,0 +1,110 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { initTestEnv } from "@Tests/utils"; +import type { Server } from "@Packages/message/server"; +import type { WindowMessage } from "@Packages/message/window_message"; +import type { ScriptLoadInfo } from "../service_worker/types"; +import type { TExtensionEnv } from "../extension/extension_env"; + +// 单测重点:sandbox runtime.execScript 中新加的 run-in 过滤逻辑 +// 通过 mock BgExecScriptWarp 与 offscreen client,观察是否构造执行实例来判断过滤是否生效 +const mockExec = vi.fn().mockResolvedValue(undefined); +const mockStop = vi.fn(); +const BgExecScriptWarpCtor = vi.fn().mockImplementation(() => ({ + exec: mockExec, + stop: mockStop, + scriptRes: { uuid: "uuid-bg" }, + valueUpdate: vi.fn(), + emitEvent: vi.fn(), +})); + +vi.mock("../content/exec_warp", () => ({ + BgExecScriptWarp: function (...args: any[]) { + return BgExecScriptWarpCtor(...args); + }, + CATRetryError: class CATRetryError {}, +})); + +vi.mock("../offscreen/client", () => ({ + proxyUpdateRunStatus: vi.fn(), +})); + +import { Runtime } from "./runtime"; + +initTestEnv(); + +const buildScript = (runIn?: string): ScriptLoadInfo => + ({ + uuid: `uuid-${runIn ?? "none"}`, + name: "bg", + type: 2, + code: "", + sourceCode: "", + metadata: runIn ? { "run-in": [runIn] } : {}, + metadataStr: "", + userConfig: {}, + userConfigStr: "", + value: {}, + resource: {}, + }) as unknown as ScriptLoadInfo; + +const setup = (extensionEnv: TExtensionEnv | undefined) => { + const windowMessage = {} as WindowMessage; + const api = {} as Server; + return new Runtime(windowMessage, api, Promise.resolve(extensionEnv)); +}; + +describe("Runtime.execScript run-in 过滤", () => { + beforeEach(() => { + BgExecScriptWarpCtor.mockClear(); + mockExec.mockClear(); + mockStop.mockClear(); + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("metadata 没有 run-in 时无论 incognito 状态都执行", async () => { + const runtime = setup({ inIncognitoContext: false }); + await runtime.execScript(buildScript()); + expect(BgExecScriptWarpCtor).toHaveBeenCalledTimes(1); + }); + + it('run-in === "all" 时无论 incognito 状态都执行', async () => { + const runtime = setup({ inIncognitoContext: true }); + await runtime.execScript(buildScript("all")); + expect(BgExecScriptWarpCtor).toHaveBeenCalledTimes(1); + }); + + it('run-in === "normal-tabs" 在普通环境中执行', async () => { + const runtime = setup({ inIncognitoContext: false }); + await runtime.execScript(buildScript("normal-tabs")); + expect(BgExecScriptWarpCtor).toHaveBeenCalledTimes(1); + }); + + it('run-in === "normal-tabs" 在隐身环境中跳过执行', async () => { + const runtime = setup({ inIncognitoContext: true }); + await runtime.execScript(buildScript("normal-tabs")); + expect(BgExecScriptWarpCtor).not.toHaveBeenCalled(); + }); + + it('run-in === "incognito-tabs" 在普通环境中跳过执行', async () => { + const runtime = setup({ inIncognitoContext: false }); + await runtime.execScript(buildScript("incognito-tabs")); + expect(BgExecScriptWarpCtor).not.toHaveBeenCalled(); + }); + + it('run-in === "incognito-tabs" 在隐身环境中执行', async () => { + const runtime = setup({ inIncognitoContext: true }); + await runtime.execScript(buildScript("incognito-tabs")); + expect(BgExecScriptWarpCtor).toHaveBeenCalledTimes(1); + }); + + it("extensionEnv 为 undefined 时,过滤被跳过照常执行(fail-open)", async () => { + // 当前实现:拿不到 incognito 状态时不做过滤,避免静默丢失任务 + const runtime = setup(undefined); + await runtime.execScript(buildScript("incognito-tabs")); + expect(BgExecScriptWarpCtor).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/app/service/sandbox/runtime.ts b/src/app/service/sandbox/runtime.ts index a96f2951d..0cf6ede9f 100644 --- a/src/app/service/sandbox/runtime.ts +++ b/src/app/service/sandbox/runtime.ts @@ -21,6 +21,7 @@ import { parseUserConfig } from "@App/pkg/utils/yaml"; import { decodeRValue } from "@App/pkg/utils/message_value"; import { extractCronExpr } from "@App/pkg/utils/cron"; import { changeLanguage, initLanguage, t } from "@App/locales/locales"; +import { type TExtensionEnv } from "../extension/extension_env"; const utime_1min = 60 * 1000; const utime_1hr = 60 * 60 * 1000; @@ -40,7 +41,8 @@ export class Runtime { constructor( private windowMessage: WindowMessage, - private api: Server + private api: Server, + private readonly extensionEnvAsync: Promise ) { this.logger = LoggerCore.getInstance().logger({ component: "sandbox" }); // 重试队列,5s检查一次 @@ -171,7 +173,20 @@ export class Runtime { // 暂未实现执行完成后立马释放,会在下一次执行时释放 await this.stopScript(script.uuid); } - const exec = new BgExecScriptWarp(script, this.windowMessage); + const extensionEnv = await this.extensionEnvAsync; + + // 判断 run-in + const runIn = script.metadata?.["run-in"]?.[0]; + const inIncognitoContext = extensionEnv?.inIncognitoContext; + if (runIn && runIn !== "all" && typeof inIncognitoContext === "boolean") { + // 判断插件运行环境 + const contextType = inIncognitoContext ? "incognito-tabs" : "normal-tabs"; + if (runIn !== contextType) { + return; + } + } + + const exec = new BgExecScriptWarp(script, this.windowMessage, extensionEnv); this.execScriptMap.set(script.uuid, exec); proxyUpdateRunStatus(this.windowMessage, { uuid: script.uuid, runStatus: SCRIPT_RUN_STATUS_RUNNING }); // 修改掉脚本掉最后运行时间, 数据库也需要修改 diff --git a/src/app/service/service_worker/index.ts b/src/app/service/service_worker/index.ts index 72ddd111d..22f71cff4 100644 --- a/src/app/service/service_worker/index.ts +++ b/src/app/service/service_worker/index.ts @@ -21,6 +21,7 @@ import { FaviconDAO } from "@App/app/repo/favicon"; import { onRegularUpdateCheckAlarm } from "./regular_updatecheck"; import { cacheInstance } from "@App/app/cache"; import { InfoNotification } from "./utils"; +import { extensionEnv, getExtensionUserAgentData } from "../extension/extension_env"; // service worker的管理器 export default class ServiceWorkerManager { @@ -36,8 +37,17 @@ export default class ServiceWorkerManager { dao.save(data); } + async getExtensionEnv(data: { requireUAD: boolean }) { + const result = { ...extensionEnv }; + if (data.requireUAD) { + result.userAgentData = await getExtensionUserAgentData(); + } + return result; + } + initManager() { this.api.on("logger", this.logger.bind(this)); + this.api.on("getExtensionEnv", this.getExtensionEnv.bind(this)); this.api.on("preparationOffscreen", async () => { // 准备好环境 await this.sender.init(); diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 8ddc0dc00..e00e6fa97 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -53,6 +53,7 @@ import type { CompiledResource, ResourceType } from "@App/app/repo/resource"; import { CompiledResourceDAO } from "@App/app/repo/resource"; import { setOnTabURLChanged } from "./url_monitor"; import { scriptToMenu, type TPopupPageLoadInfo } from "./popup_scriptmenu"; +import { getExtensionUserAgentData } from "../extension/extension_env"; const ORIGINAL_URLMATCH_SUFFIX = "{ORIGINAL}"; // 用于标记原始URLPatterns的后缀 @@ -154,26 +155,7 @@ export class RuntimeService { } async initUserAgentData() { - // @ts-ignore - const userAgentData = navigator.userAgentData; - if (userAgentData) { - this.userAgentData = { - brands: userAgentData.brands, - mobile: userAgentData.mobile, - platform: userAgentData.platform, - }; - // 处理architecture和bitness - if (chrome.runtime.getPlatformInfo) { - try { - const platformInfo = await chrome.runtime.getPlatformInfo(); - this.userAgentData.architecture = platformInfo.nacl_arch; - this.userAgentData.bitness = platformInfo.arch.includes("64") ? "64" : "32"; - } catch (e) { - // 避免 API 无法执行的问题。不影响整体运作 - console.warn(e); - } - } - } + this.userAgentData = (await getExtensionUserAgentData()) || {}; } async showUserscriptActivationGuide() { diff --git a/src/content.ts b/src/content.ts index 640a4daf1..094f345ac 100644 --- a/src/content.ts +++ b/src/content.ts @@ -7,10 +7,11 @@ import type { Message } from "@Packages/message/types"; import { getEventFlag } from "@Packages/message/common"; import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; +import { type TExtensionEnv } from "./app/service/extension/extension_env"; const messageFlag = process.env.SC_RANDOM_KEY!; -getEventFlag(messageFlag, (eventFlag: string) => { +getEventFlag(messageFlag, (eventFlag: string, extensionEnv: TExtensionEnv | undefined) => { const scriptEnvTag = ScriptEnvTag.content; const msg: Message = new CustomEventMessage(eventFlag, false, scriptEnvTag); @@ -26,7 +27,7 @@ getEventFlag(messageFlag, (eventFlag: string) => { const server = new Server("content", msg); const scriptExecutor = new ScriptExecutor(msg, new CustomEventMessage(eventFlag, true, scriptEnvTag)); - const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor); + const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, extensionEnv); runtime.contentInit(); runtime.init(); }); diff --git a/src/inject.ts b/src/inject.ts index 2f397ba95..0d13290da 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -7,10 +7,11 @@ import type { Message } from "@Packages/message/types"; import { getEventFlag } from "@Packages/message/common"; import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; +import { type TExtensionEnv } from "./app/service/extension/extension_env"; const messageFlag = process.env.SC_RANDOM_KEY!; -getEventFlag(messageFlag, (eventFlag: string) => { +getEventFlag(messageFlag, (eventFlag: string, extensionEnv: TExtensionEnv | undefined) => { const scriptEnvTag = ScriptEnvTag.inject; const msg: Message = new CustomEventMessage(eventFlag, false, scriptEnvTag); @@ -26,7 +27,7 @@ getEventFlag(messageFlag, (eventFlag: string) => { const server = new Server("inject", msg); const scriptExecutor = new ScriptExecutor(msg, new CustomEventMessage(eventFlag, true, ScriptEnvTag.content)); - const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor); + const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, extensionEnv); runtime.init(); // inject环境,直接判断白名单,注入对外接口 diff --git a/src/scripting.ts b/src/scripting.ts index f2bdd0ab8..fa943e7ba 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -7,11 +7,12 @@ import { ScriptEnvTag } from "@Packages/message/consts"; import { Server } from "@Packages/message/server"; import ScriptingRuntime from "./app/service/content/scripting"; import { negotiateEventFlag } from "@Packages/message/common"; +import { extensionEnv } from "./app/service/extension/extension_env"; const messageFlag = process.env.SC_RANDOM_KEY!; // 将初始化流程完成后,将EventFlag通知到其他环境 -negotiateEventFlag(messageFlag, 2, (eventFlag) => { +negotiateEventFlag(messageFlag, extensionEnv, 2, (eventFlag) => { // 建立与service_worker页面的连接 const extMsgComm: Message = new ExtensionMessage(false); // 初始化日志组件