From 2396bb5353626b53a38ebe636fed4307fce28226 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:15:07 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=E4=BB=A5=E7=B1=BB=E4=BC=BCbroadcast?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=E9=87=8D=E6=9E=84=E9=80=9A=E8=AE=AF=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/custom_event_message.ts | 122 ++++++++++------- packages/message/server.test.ts | 14 +- rspack.config.ts | 1 + src/app/service/content/content.ts | 28 ++-- src/app/service/content/utils.ts | 3 +- src/app/service/service_worker/runtime.ts | 66 +++++----- src/app/service/service_worker/value.ts | 53 ++++---- src/content.ts | 151 ++++++++++++++++------ src/inject.ts | 42 +++++- src/scripting.ts | 67 ++++++++++ 10 files changed, 385 insertions(+), 162 deletions(-) create mode 100644 src/scripting.ts diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index b3e8cf952..dc27d4fcf 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -1,13 +1,22 @@ import type { Message, MessageConnect, RuntimeMessageSender, TMessage } from "./types"; import { v4 as uuidv4 } from "uuid"; import { type PostMessage, type WindowMessageBody, WindowMessageConnect } from "./window_message"; -import LoggerCore from "@App/app/logger/core"; import EventEmitter from "eventemitter3"; import { DefinedFlags } from "@App/app/service/service_worker/runtime.consts"; // 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败 -const pageDispatchEvent = performance.dispatchEvent.bind(performance); -const pageAddEventListener = performance.addEventListener.bind(performance); +export const pageDispatchEvent = performance.dispatchEvent.bind(performance); +export const pageAddEventListener = performance.addEventListener.bind(performance); +export const pageRemoveEventListener = performance.removeEventListener.bind(performance); +const detailClone = typeof cloneInto === "function" ? cloneInto : null; +export const pageDispatchCustomEvent = (eventType: string, detail: any) => { + if (detailClone && detail) detail = detailClone(detail, document.defaultView); + const ev = new CustomEventClone(eventType, { + detail, + cancelable: true, + }); + return pageDispatchEvent(ev); +}; // 避免页面载入后改动全域物件导致消息传递失败 const MouseEventClone = MouseEvent; @@ -30,28 +39,59 @@ export class CustomEventPostMessage implements PostMessage { } } +export type PageMessaging = { + et: string; + bindEmitter?: () => void; + waitReady?: Promise; + waitReadyResolve?: () => any; + onReady?: (callback: () => any) => any; +}; + +export const createPageMessaging = (et: string) => { + const pageMessaging = { et } as PageMessaging; + pageMessaging.waitReady = new Promise((resolve) => { + pageMessaging.waitReadyResolve = resolve; + }); + pageMessaging.onReady = (callback: () => any) => { + if (pageMessaging.et) { + callback(); + } else { + pageMessaging.waitReady!.then(callback); + } + }; + return pageMessaging; +}; + // 使用CustomEvent来进行通讯, 可以在content与inject中传递一些dom对象 export class CustomEventMessage implements Message { EE = new EventEmitter(); readonly receiveFlag: string; readonly sendFlag: string; + readonly pageMessagingHandler: (event: Event) => any; // 关联dom目标 relatedTarget: Map = new Map(); constructor( - messageFlag: string, + private pageMessaging: PageMessaging, protected readonly isContent: boolean ) { - this.receiveFlag = `evt${messageFlag}${isContent ? DefinedFlags.contentFlag : DefinedFlags.injectFlag}${DefinedFlags.domEvent}`; - this.sendFlag = `evt${messageFlag}${isContent ? DefinedFlags.injectFlag : DefinedFlags.contentFlag}${DefinedFlags.domEvent}`; - pageAddEventListener(this.receiveFlag, (event) => { + this.receiveFlag = `${isContent ? DefinedFlags.contentFlag : DefinedFlags.injectFlag}${DefinedFlags.domEvent}`; + this.sendFlag = `${isContent ? DefinedFlags.injectFlag : DefinedFlags.contentFlag}${DefinedFlags.domEvent}`; + this.pageMessagingHandler = (event: Event) => { if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) { - relatedTargetMap.set(event.movementX, event.relatedTarget!); + relatedTargetMap.set(event.movementX, event.relatedTarget); } else if (event instanceof CustomEventClone) { this.messageHandle(event.detail, new CustomEventPostMessage(this)); } - }); + }; + } + + bindEmitter() { + if (!this.pageMessaging.et) throw new Error("bindEmitter() failed"); + const receiveFlag = `evt_${this.pageMessaging.et}_${this.receiveFlag}`; + pageRemoveEventListener(receiveFlag, this.pageMessagingHandler); // 避免重覆 + pageAddEventListener(receiveFlag, this.pageMessagingHandler); } messageHandle(data: WindowMessageBody, target: PostMessage) { @@ -95,49 +135,41 @@ export class CustomEventMessage implements Message { connect(data: TMessage): Promise { return new Promise((resolve) => { - const body: WindowMessageBody = { - messageId: uuidv4(), - type: "connect", - data, - }; - this.nativeSend(body); - // EventEmitter3 采用同步事件设计,callback会被马上执行而不像传统javascript架构以下一个macrotask 执行 - resolve(new WindowMessageConnect(body.messageId, this.EE, new CustomEventPostMessage(this))); + this.pageMessaging.onReady!(() => { + const body: WindowMessageBody = { + messageId: uuidv4(), + type: "connect", + data, + }; + this.nativeSend(body); + // EventEmitter3 采用同步事件设计,callback会被马上执行而不像传统javascript架构以下一个macrotask 执行 + resolve(new WindowMessageConnect(body.messageId, this.EE, new CustomEventPostMessage(this))); + }); }); } nativeSend(detail: any) { - if (typeof cloneInto !== "undefined") { - try { - LoggerCore.logger().info("nativeSend"); - detail = cloneInto(detail, document.defaultView); - } catch (e) { - console.log(e); - LoggerCore.logger().info("error data"); - } - } - - const ev = new CustomEventClone(this.sendFlag, { - detail, - }); - pageDispatchEvent(ev); + if (!this.pageMessaging.et) throw new Error("inject.js is not ready or destroyed."); + pageDispatchCustomEvent(`evt_${this.pageMessaging.et}_${this.sendFlag}`, detail); } sendMessage(data: TMessage): Promise { return new Promise((resolve: ((value: T) => void) | null) => { - const messageId = uuidv4(); - const body: WindowMessageBody = { - messageId, - type: "sendMessage", - data, - }; - const eventId = `response:${messageId}`; - this.EE.addListener(eventId, (body: WindowMessageBody) => { - this.EE.removeAllListeners(eventId); - resolve!(body.data as T); - resolve = null; // 设为 null 提醒JS引擎可以GC + this.pageMessaging.onReady!(() => { + const messageId = uuidv4(); + const body: WindowMessageBody = { + messageId, + type: "sendMessage", + data, + }; + const eventId = `response:${messageId}`; + this.EE.addListener(eventId, (body: WindowMessageBody) => { + this.EE.removeAllListeners(eventId); + resolve!(body.data as T); + resolve = null; // 设为 null 提醒JS引擎可以GC + }); + this.nativeSend(body); }); - this.nativeSend(body); }); } @@ -145,6 +177,7 @@ export class CustomEventMessage implements Message { // 与content页的消息通讯实际是同步,此方法不需要经过background // 但是请注意中间不要有promise syncSendMessage(data: TMessage): TMessage { + if (!this.pageMessaging.et) throw new Error("inject.js is not ready or destroyed."); const messageId = uuidv4(); const body: WindowMessageBody = { messageId, @@ -164,11 +197,12 @@ export class CustomEventMessage implements Message { } sendRelatedTarget(target: EventTarget): number { + if (!this.pageMessaging.et) throw new Error("inject.js is not ready or destroyed."); // 特殊处理relatedTarget,返回id进行关联 // 先将relatedTarget转换成id发送过去 const id = (relateId = relateId === maxInteger ? 1 : relateId + 1); // 可以使用此种方式交互element - const ev = new MouseEventClone(this.sendFlag, { + const ev = new MouseEventClone(`evt_${this.pageMessaging.et}_${this.sendFlag}`, { movementX: id, relatedTarget: target, }); diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index 5347ed823..6fa3e6fa2 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -1,8 +1,9 @@ import { describe, expect, it, beforeEach, vi, afterEach } from "vitest"; import { GetSenderType, SenderConnect, SenderRuntime, Server, type IGetSender } from "./server"; -import { CustomEventMessage } from "./custom_event_message"; +import { createPageMessaging, CustomEventMessage } from "./custom_event_message"; import type { MessageConnect, RuntimeMessageSender } from "./types"; import { DefinedFlags } from "@App/app/service/service_worker/runtime.consts"; +import { uuidv4 } from "@App/pkg/utils/uuid"; let contentMessage: CustomEventMessage; let injectMessage: CustomEventMessage; @@ -12,10 +13,13 @@ let client: CustomEventMessage; const nextTick = () => Promise.resolve().then(() => {}); const setupGlobal = () => { - const flags = "-test.server"; + const testFlag = uuidv4(); + const testPageMessaging = createPageMessaging(testFlag); // 创建 content 和 inject 之间的消息通道 - contentMessage = new CustomEventMessage(flags, true); // content 端 - injectMessage = new CustomEventMessage(flags, false); // inject 端 + contentMessage = new CustomEventMessage(testPageMessaging, true); // content 端 + injectMessage = new CustomEventMessage(testPageMessaging, false); // inject 端 + contentMessage.bindEmitter(); + injectMessage.bindEmitter(); // 服务端使用 content 消息 server = new Server("api", contentMessage); @@ -33,7 +37,7 @@ const setupGlobal = () => { vi.fn().mockImplementation((event: Event) => { if (event instanceof CustomEvent) { const eventType = event.type; - if (eventType.includes("-test.server")) { + if (eventType.includes(testFlag)) { let targetEventType: string; let messageThis: CustomEventMessage; let messageThat: CustomEventMessage; diff --git a/rspack.config.ts b/rspack.config.ts index f0fe226a7..c7bdd6fe2 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -34,6 +34,7 @@ export default defineConfig({ offscreen: `${src}/offscreen.ts`, sandbox: `${src}/sandbox.ts`, content: `${src}/content.ts`, + scripting: `${src}/scripting.ts`, inject: `${src}/inject.ts`, popup: `${src}/pages/popup/main.tsx`, install: `${src}/pages/install/main.tsx`, diff --git a/src/app/service/content/content.ts b/src/app/service/content/content.ts index ec39006c4..59ed5f04e 100644 --- a/src/app/service/content/content.ts +++ b/src/app/service/content/content.ts @@ -1,4 +1,4 @@ -import { Client, sendMessage } from "@Packages/message/client"; +import { Client } from "@Packages/message/client"; import { type CustomEventMessage } from "@Packages/message/custom_event_message"; import { forwardMessage, type Server } from "@Packages/message/server"; import type { MessageSend } from "@Packages/message/types"; @@ -16,7 +16,7 @@ export default class ContentRuntime { constructor( // 监听来自service_worker的消息 - private readonly extServer: Server, + private readonly extServer: null, // 监听来自inject的消息 private readonly server: Server, // 发送给扩展service_worker的通信接口 @@ -29,16 +29,16 @@ export default class ContentRuntime { ) {} init() { - this.extServer.on("runtime/emitEvent", (data) => { - // 转发给inject和scriptExecutor - this.scriptExecutor.emitEvent(data); - return sendMessage(this.senderToInject, "inject/runtime/emitEvent", data); - }); - this.extServer.on("runtime/valueUpdate", (data) => { - // 转发给inject和scriptExecutor - this.scriptExecutor.valueUpdate(data); - return sendMessage(this.senderToInject, "inject/runtime/valueUpdate", data); - }); + // this.extServer.on("runtime/emitEvent", (data) => { + // // 转发给inject和scriptExecutor + // this.scriptExecutor.emitEvent(data); + // return sendMessage(this.senderToInject, "inject/runtime/emitEvent", data); + // }); + // this.extServer.on("runtime/valueUpdate", (data) => { + // // 转发给inject和scriptExecutor + // this.scriptExecutor.valueUpdate(data); + // return sendMessage(this.senderToInject, "inject/runtime/valueUpdate", data); + // }); this.server.on("logger", (data: Logger) => { LoggerCore.logger().log(data.level, data.message, data.label); }); @@ -127,8 +127,8 @@ export default class ContentRuntime { ); } - pageLoad(messageFlag: string, envInfo: GMInfoEnv) { - this.scriptExecutor.checkEarlyStartScript("content", messageFlag, envInfo); + pageLoad(envInfo: GMInfoEnv) { + this.scriptExecutor.checkEarlyStartScript("content", MessageFlag, envInfo); const client = new RuntimeClient(this.senderToExt); // 向service_worker请求脚本列表及环境信息 client.pageLoad().then((o) => { diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index 0c2c1c5c4..54a2e8bed 100644 --- a/src/app/service/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -151,7 +151,8 @@ export function compilePreInjectScript( return `window['${flag}'] = function(){${autoDeleteMountCode}${scriptCode}}; { let o = { cancelable: true, detail: { scriptFlag: '${flag}', scriptInfo: (${scriptInfoJSON}) } }, - f = () => performance.dispatchEvent(new CustomEvent('${evScriptLoad}', o)), + c = typeof cloneInto === "function" ? cloneInto(o, document.defaultView) : o, + f = () => performance.dispatchEvent(new CustomEvent('${evScriptLoad}', c)), needWait = f(); if (needWait) performance.addEventListener('${evEnvLoad}', f, { once: true }); } diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index c8c2e60f7..46cf18a2d 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -18,11 +18,9 @@ import { } from "./utils"; import { checkUserScriptsAvailable, - randomMessageFlag, getMetadataStr, getUserConfigStr, obtainBlackList, - isFirefox, sourceMapTo, } from "@App/pkg/utils/utils"; import { cacheInstance } from "@App/app/cache"; @@ -52,10 +50,11 @@ 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 { uuidv4 } from "@App/pkg/utils/uuid"; // 避免使用版本号控制导致代码理解混乱 // 用来清除 UserScript API 里的旧缓存 -const USERSCRIPTS_REGISTER_CONTROL = "92292a62-4e81-4dc3-87d0-cb0f0cb9883d"; +const USERSCRIPTS_REGISTER_CONTROL = "92292a62-5e81-3dc3-87d0-cb0f0cb9883e"; const ORIGINAL_URLMATCH_SUFFIX = "{ORIGINAL}"; // 用于标记原始URLPatterns的后缀 @@ -137,7 +136,9 @@ export class RuntimeService { .get("scriptInjectMessageFlag") .then((res) => { runtimeGlobal.messageFlag = res?.value || this.generateMessageFlag(); - return this.localStorageDAO.save({ key: "scriptInjectMessageFlag", value: runtimeGlobal.messageFlag }); + if (runtimeGlobal.messageFlag !== res?.value) { + return this.localStorageDAO.save({ key: "scriptInjectMessageFlag", value: runtimeGlobal.messageFlag }); + } }) .catch(console.error); this.logger = LoggerCore.logger({ component: "runtime" }); @@ -666,13 +667,15 @@ export class RuntimeService { chrome.userScripts?.unregister(), chrome.scripting.unregisterContentScripts(), this.localStorageDAO.save({ key: "scriptInjectMessageFlag", value: runtimeGlobal.messageFlag }), + chrome.storage.session.set({ unregisterUserscriptsFlag: `${Date.now()}.${Math.random()}` }), ]); } } // 生成messageFlag generateMessageFlag(): string { - return randomMessageFlag(); + // return randomMessageFlag(); + return uuidv4(); } getMessageFlag() { @@ -846,34 +849,31 @@ export class RuntimeService { } // Note: Chrome does not support file.js?query // 注意:Chrome 不支持 file.js?query - if (isFirefox()) { - // 使用 URLSearchParams 避免字符编码问题 - retContent = [ - { - id: "scriptcat-content", - js: [`/src/content.js?${new URLSearchParams({ usp_flag: messageFlag })}&usp_end`], - matches: [""], - allFrames: true, - runAt: "document_start", - excludeMatches, - } satisfies chrome.scripting.RegisteredContentScript, - ]; - } else { - const contentJs = await this.getContentJsCode(); - if (contentJs) { - const codeBody = `(function (MessageFlag) {\n${contentJs}\n})('${messageFlag}')`; - const code = `${codeBody}${sourceMapTo("scriptcat-content.js")}\n`; - retInject.push({ - id: "scriptcat-content", - js: [{ code }], - matches: [""], - allFrames: true, - runAt: "document_start", - world: "USER_SCRIPT", - excludeMatches, - excludeGlobs, - } satisfies chrome.userScripts.RegisteredUserScript); - } + retContent = [ + { + id: "scriptcat-content", + js: ["/src/scripting.js"], + matches: [""], + allFrames: true, + runAt: "document_start", + excludeMatches, + } satisfies chrome.scripting.RegisteredContentScript, + ]; + + const contentJs = await this.getContentJsCode(); + if (contentJs) { + const codeBody = `(function (MessageFlag) {\n${contentJs}\n})('${messageFlag}')`; + const code = `${codeBody}${sourceMapTo("scriptcat-content.js")}\n`; + retInject.push({ + id: "scriptcat-content", + js: [{ code }], + matches: [""], + allFrames: true, + runAt: "document_start", + world: "USER_SCRIPT", + excludeMatches, + excludeGlobs, + } satisfies chrome.userScripts.RegisteredUserScript); } return { content: retContent, inject: retInject }; diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index 42f539294..be3403367 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -77,31 +77,36 @@ export class ValueService { // 推送值到tab async pushValueToTab(sendData: T) { - const { storageName } = sendData; - chrome.tabs.query({}, (tabs) => { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error("chrome.runtime.lastError in chrome.tabs.query:", lastError); - // 没有 tabs 资讯,无法发推送到 tabs - return; - } - // 推送到所有加载了本脚本的tab中 - for (const tab of tabs) { - const tabId = tab.id; - if (tab.discarded || !tabId) continue; - this.popup!.getScriptMenu(tabId).then((scriptMenu) => { - if (scriptMenu.find((item) => item.storageName === storageName)) { - this.runtime!.sendMessageToTab( - { - tabId, - }, - "valueUpdate", - sendData - ); - } - }); - } + // const { storageName } = sendData; + chrome.storage.local.set({ + valueUpdateDelivery: { + rId: `${Date.now()}.${Math.random()}`, + sendData, + }, }); + // chrome.tabs.query({}, (tabs) => { + // const lastError = chrome.runtime.lastError; + // if (lastError) { + // console.error("chrome.runtime.lastError in chrome.tabs.query:", lastError); + // // 没有 tabs 资讯,无法发推送到 tabs + // return; + // } + // // 推送到所有加载了本脚本的tab中 + // for (const tab of tabs) { + // const tabId = tab.id!; + // this.popup!.getScriptMenu(tabId).then((scriptMenu) => { + // if (scriptMenu.find((item) => item.storageName === storageName)) { + // this.runtime!.sendMessageToTab( + // { + // tabId, + // }, + // "valueUpdate", + // sendData + // ); + // } + // }); + // } + // }); // 推送到offscreen中 this.runtime!.sendMessageToTab( { diff --git a/src/content.ts b/src/content.ts index 2e4beb5c0..b5aaf86aa 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,51 +1,126 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; import { ExtensionMessage } from "@Packages/message/extension_message"; -import { CustomEventMessage } from "@Packages/message/custom_event_message"; +import { + CustomEventMessage, + createPageMessaging, + pageAddEventListener, + pageDispatchCustomEvent, +} from "@Packages/message/custom_event_message"; import { Server } from "@Packages/message/server"; import ContentRuntime from "./app/service/content/content"; import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_executor"; -import { randomMessageFlag, getUspMessageFlag } from "./pkg/utils/utils"; import type { Message } from "@Packages/message/types"; +import { sendMessage } from "@Packages/message/client"; +import type { ValueUpdateDataEncoded } from "./app/service/content/types"; +import { uuidv4, uuidv5 } from "./pkg/utils/uuid"; -// @ts-ignore -const MessageFlag: string | null = (typeof arguments === "object" && arguments?.[0]) || getUspMessageFlag(); - -if (!MessageFlag) { - console.error("MessageFlag is unavailable."); -} else if (typeof chrome?.runtime?.onMessage?.addListener !== "function") { - // Firefox userScripts.RegisteredUserScript does not provide chrome.runtime.onMessage.addListener - // Firefox scripting.RegisteredContentScript does provide chrome.runtime.onMessage.addListener - // Firefox 的 userScripts.RegisteredUserScript 不提供 chrome.runtime.onMessage.addListener - // Firefox 的 scripting.RegisteredContentScript 提供 chrome.runtime.onMessage.addListener - console.error("chrome.runtime.onMessage.addListener is not a function"); -} else { - // 建立与service_worker页面的连接 - const extMsgComm: Message = new ExtensionMessage(false); - // 初始化日志组件 - const loggerCore = new LoggerCore({ - writer: new MessageWriter(extMsgComm, "serviceWorker/logger"), - labels: { env: "content" }, - }); +/* global MessageFlag */ + +const mainKey = uuidv5("scriptcat-listen-inject", MessageFlag); + +const contentRandomId = uuidv4(); + +let scriptingMessagingBind = () => {}; +// ------------ 對象 ------------ +class ImmutableEventTarget extends EventTarget {} +ImmutableEventTarget.prototype.addEventListener = EventTarget.prototype.addEventListener; +ImmutableEventTarget.prototype.dispatchEvent = EventTarget.prototype.dispatchEvent; +ImmutableEventTarget.prototype.removeEventListener = EventTarget.prototype.removeEventListener; + +const pageMessaging = createPageMessaging(""); +const scriptExecutorPageMessaging = createPageMessaging(uuidv4()); + +const scriptingMessaging = createPageMessaging(""); + +const emitters = new Map(); - loggerCore.logger().debug("content start"); +const msgInject = new CustomEventMessage(pageMessaging, true); - const msgInject = new CustomEventMessage(MessageFlag, true); +// ------------ 監聽 ------------ - // 处理scriptExecutor - const scriptExecutorFlag = randomMessageFlag(); - const scriptExecutorMsg = new CustomEventMessage(scriptExecutorFlag, true); - const scriptExecutor = new ScriptExecutor(new CustomEventMessage(scriptExecutorFlag, false)); +performance.addEventListener(mainKey, (ev) => { + // 注:即使外部執行 "scriptcat-listen-inject", 不知道 inject.ts 的亂數 flag 是不可能截取資料 + if (ev instanceof CustomEvent && typeof ev.detail?.injectFlagEvt === "string") { + // 必定由 inject.ts 要求 + ev.preventDefault(); // dispatchEvent 返回 false + // 按 inject.ts 要求返回 emitter + const { injectFlagEvt, scripting } = ev.detail; + let emitter = emitters.get(injectFlagEvt); + if (!emitter) { + emitters.set(injectFlagEvt, (emitter = uuidv5(injectFlagEvt, contentRandomId))); + } + if (scripting) { + scriptingMessaging.et = emitter; + scriptingMessagingBind(); + } else { + pageMessaging.et = emitter; + msgInject.bindEmitter(); + } + // 傳送 emitter 給 inject.ts + pageDispatchCustomEvent(`${injectFlagEvt}`, { + [`emitterKeyFor${injectFlagEvt}`]: emitter, + }); + } +}); - const server = new Server("content", [msgInject, scriptExecutorMsg]); +// ------------ 连接 ------------ + +// 建立与service_worker页面的连接 +const extMsgComm: Message = new ExtensionMessage(false); +// 初始化日志组件 +const loggerCore = new LoggerCore({ + writer: new MessageWriter(extMsgComm, "serviceWorker/logger"), + labels: { env: "content" }, +}); + +loggerCore.logger().debug("content start"); + +// 处理scriptExecutor +const scriptExecutorMsg1 = new CustomEventMessage(scriptExecutorPageMessaging, true); +scriptExecutorMsg1.bindEmitter(); +const scriptExecutorMsg2 = new CustomEventMessage(scriptExecutorPageMessaging, false); +scriptExecutorMsg2.bindEmitter(); +const scriptExecutor = new ScriptExecutor(scriptExecutorMsg2); + +const server = new Server("content", [msgInject, scriptExecutorMsg1]); + +// Opera中没有chrome.runtime.onConnect,并且content也不需要chrome.runtime.onConnect +// 所以不需要处理连接,设置为false +// const extServer = new Server("content", extMsgComm, false); +// scriptExecutor的消息接口 +// 初始化运行环境 +const runtime = new ContentRuntime(null, server, extMsgComm, msgInject, scriptExecutorMsg1, scriptExecutor); +runtime.init(); +// 页面加载,注入脚本 +runtime.pageLoad(initEnvInfo); + +scriptingMessagingBind = () => { + if (!scriptingMessaging.et) throw new Error("scriptingMessaging is not ready or destroyed"); + pageAddEventListener(`evt_${scriptingMessaging.et}_deliveryMessage`, (ev) => { + if (ev instanceof CustomEvent) { + const { tag, value } = ev.detail; + if (tag === "localStorage:scriptInjectMessageFlag") { + // 反注册所有脚本时,同时中断网页信息传递 + pageMessaging.et = ""; + scriptExecutorPageMessaging.et = ""; + scriptingMessaging.et = ""; + } else if (tag === "valueUpdateDelivery") { + // const storageName = sendData.storageName; + // 转发给inject和scriptExecutor + const sendData = value.sendData as ValueUpdateDataEncoded; + scriptExecutor.valueUpdate(sendData); + sendMessage(msgInject, "inject/runtime/valueUpdate", sendData); + } else if (tag === "content/runtime/emitEvent") { + const data = value; + // 转发给inject和scriptExecutor + scriptExecutor.emitEvent(data); + sendMessage(msgInject, "inject/runtime/emitEvent", data); + } + } + }); +}; - // Opera中没有chrome.runtime.onConnect,并且content也不需要chrome.runtime.onConnect - // 所以不需要处理连接,设置为false - const extServer = new Server("content", extMsgComm, false); - // scriptExecutor的消息接口 - // 初始化运行环境 - const runtime = new ContentRuntime(extServer, server, extMsgComm, msgInject, scriptExecutorMsg, scriptExecutor); - runtime.init(); - // 页面加载,注入脚本 - runtime.pageLoad(MessageFlag, initEnvInfo); -} +// ------------ 請求 ------------ +performance.dispatchEvent(new CustomEvent(mainKey)); +// ----------------------------- diff --git a/src/inject.ts b/src/inject.ts index b434858d1..d7ea191a5 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -1,16 +1,25 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; -import { CustomEventMessage } from "@Packages/message/custom_event_message"; +import { + CustomEventMessage, + createPageMessaging, + pageDispatchCustomEvent, +} from "@Packages/message/custom_event_message"; import { Server } from "@Packages/message/server"; import type { TScriptInfo } from "./app/repo/scripts"; import type { GMInfoEnv } from "./app/service/content/types"; import { InjectRuntime } from "./app/service/content/inject"; import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_executor"; -import type { Message } from "@Packages/message/types"; +import { randomMessageFlag } from "./pkg/utils/utils"; +import { uuidv5 } from "./pkg/utils/uuid"; /* global MessageFlag */ -const msg: Message = new CustomEventMessage(MessageFlag, false); +const mainKey = uuidv5("scriptcat-listen-inject", MessageFlag); + +const pageMessaging = createPageMessaging(""); + +const msg = new CustomEventMessage(pageMessaging, false); // 加载logger组件 const logger = new LoggerCore({ @@ -33,3 +42,30 @@ server.on("pageLoad", (data: { injectScriptList: TScriptInfo[]; envInfo: GMInfoE runtime.startScripts(data.injectScriptList, data.envInfo); runtime.onInjectPageLoaded(); }); + +const injectFlag = randomMessageFlag(); +const injectFlagEvt = injectFlag; + +// 用來接收 emitter +performance.addEventListener( + `${injectFlagEvt}`, + (ev) => { + if (ev instanceof CustomEvent && ev.detail?.[`emitterKeyFor${injectFlagEvt}`]) { + pageMessaging.et = ev.detail[`emitterKeyFor${injectFlagEvt}`]; + msg.bindEmitter(); + } + }, + { once: true } +); + +const submitTarget = () => { + return pageDispatchCustomEvent(mainKey, { injectFlagEvt }); +}; + +if (submitTarget() === true) { + performance.addEventListener(mainKey, (ev) => { + if (ev instanceof CustomEvent && !ev.detail) { + submitTarget(); + } + }); +} diff --git a/src/scripting.ts b/src/scripting.ts new file mode 100644 index 000000000..e2b467795 --- /dev/null +++ b/src/scripting.ts @@ -0,0 +1,67 @@ +import { randomMessageFlag } from "./pkg/utils/utils"; +import { createPageMessaging, pageDispatchCustomEvent } from "@Packages/message/custom_event_message"; +import { uuidv5 } from "./pkg/utils/uuid"; + +const scriptingMessaging = createPageMessaging(""); + +chrome.storage.local.get(["localStorage:scriptInjectMessageFlag"]).then((m) => { + const MessageFlag = m["localStorage:scriptInjectMessageFlag"].value; + + const mainKey = uuidv5("scriptcat-listen-inject", MessageFlag); + + const dispatchDeliveryMessage = (detail: any) => { + if (!scriptingMessaging.et) throw new Error("scriptingMessaging is not ready or destroyed"); + pageDispatchCustomEvent(`evt_${scriptingMessaging.et}_deliveryMessage`, detail); + }; + + // ------------------------------ + chrome.storage.local.onChanged.addListener((changes) => { + if (changes["localStorage:scriptInjectMessageFlag"]?.newValue) { + dispatchDeliveryMessage({ + tag: "localStorage:scriptInjectMessageFlag", + value: changes["localStorage:scriptInjectMessageFlag"]?.newValue, + }); + } + if (changes["valueUpdateDelivery"]?.newValue) { + dispatchDeliveryMessage({ + tag: "valueUpdateDelivery", + value: changes["valueUpdateDelivery"]?.newValue, + }); + } + }); + + chrome.runtime.onMessage.addListener((message, _sender) => { + if (!message) return; + const { action, data } = message; + dispatchDeliveryMessage({ + tag: action, + value: data, + }); + }); + + const injectFlag = randomMessageFlag(); + const injectFlagEvt = injectFlag; + + // 用來接收 emitter + performance.addEventListener( + `${injectFlagEvt}`, + (ev) => { + if (ev instanceof CustomEvent && ev.detail?.[`emitterKeyFor${injectFlagEvt}`]) { + scriptingMessaging.et = ev.detail[`emitterKeyFor${injectFlagEvt}`]; + } + }, + { once: true } + ); + + const submitTarget = () => { + return pageDispatchCustomEvent(mainKey, { injectFlagEvt, scripting: true }); + }; + + if (submitTarget() === true) { + performance.addEventListener(mainKey, (ev) => { + if (ev instanceof CustomEvent && !ev.detail) { + submitTarget(); + } + }); + } +}); From 531ac109177b4275220305f85c53693ebf0261c8 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:01:04 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=E4=BC=98=E5=8C=96=20scripting.ts=20?= =?UTF-8?q?=E7=9A=84=E4=BB=A3=E7=A0=81=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripting.ts | 66 +++++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/src/scripting.ts b/src/scripting.ts index e2b467795..1217e923c 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -3,42 +3,48 @@ import { createPageMessaging, pageDispatchCustomEvent } from "@Packages/message/ import { uuidv5 } from "./pkg/utils/uuid"; const scriptingMessaging = createPageMessaging(""); +const messageStack: any[] = []; + +// 在取得 scriptInjectMessageFlag 前,先堆叠一下,避免漏掉 +let dispatchDeliveryMessage = (message: any) => { + messageStack.push(message); +}; + +// ------------------------------ +chrome.storage.local.onChanged.addListener((changes) => { + if (changes["localStorage:scriptInjectMessageFlag"]?.newValue) { + dispatchDeliveryMessage({ + tag: "localStorage:scriptInjectMessageFlag", + value: changes["localStorage:scriptInjectMessageFlag"]?.newValue, + }); + } + if (changes["valueUpdateDelivery"]?.newValue) { + dispatchDeliveryMessage({ + tag: "valueUpdateDelivery", + value: changes["valueUpdateDelivery"]?.newValue, + }); + } +}); + +chrome.runtime.onMessage.addListener((message, _sender) => { + if (!message) return; + const { action, data } = message; + dispatchDeliveryMessage({ + tag: action, + value: data, + }); +}); chrome.storage.local.get(["localStorage:scriptInjectMessageFlag"]).then((m) => { const MessageFlag = m["localStorage:scriptInjectMessageFlag"].value; const mainKey = uuidv5("scriptcat-listen-inject", MessageFlag); - const dispatchDeliveryMessage = (detail: any) => { + const dispatchDeliveryMessageAfterEtSet = (detail: any) => { if (!scriptingMessaging.et) throw new Error("scriptingMessaging is not ready or destroyed"); pageDispatchCustomEvent(`evt_${scriptingMessaging.et}_deliveryMessage`, detail); }; - // ------------------------------ - chrome.storage.local.onChanged.addListener((changes) => { - if (changes["localStorage:scriptInjectMessageFlag"]?.newValue) { - dispatchDeliveryMessage({ - tag: "localStorage:scriptInjectMessageFlag", - value: changes["localStorage:scriptInjectMessageFlag"]?.newValue, - }); - } - if (changes["valueUpdateDelivery"]?.newValue) { - dispatchDeliveryMessage({ - tag: "valueUpdateDelivery", - value: changes["valueUpdateDelivery"]?.newValue, - }); - } - }); - - chrome.runtime.onMessage.addListener((message, _sender) => { - if (!message) return; - const { action, data } = message; - dispatchDeliveryMessage({ - tag: action, - value: data, - }); - }); - const injectFlag = randomMessageFlag(); const injectFlagEvt = injectFlag; @@ -48,6 +54,14 @@ chrome.storage.local.get(["localStorage:scriptInjectMessageFlag"]).then((m) => { (ev) => { if (ev instanceof CustomEvent && ev.detail?.[`emitterKeyFor${injectFlagEvt}`]) { scriptingMessaging.et = ev.detail[`emitterKeyFor${injectFlagEvt}`]; + dispatchDeliveryMessage = dispatchDeliveryMessageAfterEtSet; + if (messageStack.length > 0) { + const messages = messageStack.slice(); + messageStack.length = 0; + for (const message of messages) { + dispatchDeliveryMessage(message); + } + } } }, { once: true } From 57502783c4f0e34775b8eda0eb7727807780a9ac Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:35:29 +0900 Subject: [PATCH 03/10] =?UTF-8?q?=E5=85=B1=E9=80=9A=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/common.ts | 17 ++++++++++++++ packages/message/custom_event_message.ts | 26 +++++++--------------- src/app/service/content/script_executor.ts | 3 ++- src/content.ts | 12 ++++------ src/inject.ts | 11 ++++----- src/scripting.ts | 7 +++--- 6 files changed, 39 insertions(+), 37 deletions(-) create mode 100644 packages/message/common.ts diff --git a/packages/message/common.ts b/packages/message/common.ts new file mode 100644 index 000000000..b4757ed75 --- /dev/null +++ b/packages/message/common.ts @@ -0,0 +1,17 @@ +// 避免页面载入后改动全域物件导致消息传递失败 +export const MouseEventClone = MouseEvent; +export const CustomEventClone = CustomEvent; + +// 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败 +export const pageDispatchEvent = performance.dispatchEvent.bind(performance); +export const pageAddEventListener = performance.addEventListener.bind(performance); +export const pageRemoveEventListener = performance.removeEventListener.bind(performance); +const detailClone = typeof cloneInto === "function" ? cloneInto : null; +export const pageDispatchCustomEvent = (eventType: string, detail: any) => { + if (detailClone && detail) detail = detailClone(detail, document.defaultView); + const ev = new CustomEventClone(eventType, { + detail, + cancelable: true, + }); + return pageDispatchEvent(ev); +}; diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index dc27d4fcf..933681b29 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -3,24 +3,14 @@ import { v4 as uuidv4 } from "uuid"; import { type PostMessage, type WindowMessageBody, WindowMessageConnect } from "./window_message"; import EventEmitter from "eventemitter3"; import { DefinedFlags } from "@App/app/service/service_worker/runtime.consts"; - -// 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败 -export const pageDispatchEvent = performance.dispatchEvent.bind(performance); -export const pageAddEventListener = performance.addEventListener.bind(performance); -export const pageRemoveEventListener = performance.removeEventListener.bind(performance); -const detailClone = typeof cloneInto === "function" ? cloneInto : null; -export const pageDispatchCustomEvent = (eventType: string, detail: any) => { - if (detailClone && detail) detail = detailClone(detail, document.defaultView); - const ev = new CustomEventClone(eventType, { - detail, - cancelable: true, - }); - return pageDispatchEvent(ev); -}; - -// 避免页面载入后改动全域物件导致消息传递失败 -const MouseEventClone = MouseEvent; -const CustomEventClone = CustomEvent; +import { + pageDispatchEvent, + pageAddEventListener, + pageRemoveEventListener, + pageDispatchCustomEvent, + MouseEventClone, + CustomEventClone, +} from "@Packages/message/common"; // 避免页面载入后改动 Map.prototype 导致消息传递失败 const relatedTargetMap = new Map(); diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index 32e499bfb..34fd6dda7 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -6,6 +6,7 @@ import type { GMInfoEnv, ScriptFunc, ValueUpdateDataEncoded } from "./types"; import { addStyleSheet, definePropertyListener } from "./utils"; import type { TScriptInfo } from "@App/app/repo/scripts"; import { DefinedFlags } from "../service_worker/runtime.consts"; +import { pageDispatchEvent } from "@Packages/message/common"; export type ExecScriptEntry = { scriptLoadInfo: TScriptInfo; @@ -101,7 +102,7 @@ export class ScriptExecutor { // 通知 环境 加载完成 // 适用于此「通知环境加载完成」代码执行前的脚本加载 const ev = new CustomEvent(envLoadCompleteEvtName); - performance.dispatchEvent(ev); + pageDispatchEvent(ev); } execEarlyScript(flag: string, scriptInfo: TScriptInfo, envInfo: GMInfoEnv) { diff --git a/src/content.ts b/src/content.ts index b5aaf86aa..e34849108 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,12 +1,8 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; import { ExtensionMessage } from "@Packages/message/extension_message"; -import { - CustomEventMessage, - createPageMessaging, - pageAddEventListener, - pageDispatchCustomEvent, -} from "@Packages/message/custom_event_message"; +import { CustomEventMessage, createPageMessaging } from "@Packages/message/custom_event_message"; +import { pageAddEventListener, pageDispatchCustomEvent, pageDispatchEvent } from "@Packages/message/common"; import { Server } from "@Packages/message/server"; import ContentRuntime from "./app/service/content/content"; import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_executor"; @@ -39,7 +35,7 @@ const msgInject = new CustomEventMessage(pageMessaging, true); // ------------ 監聽 ------------ -performance.addEventListener(mainKey, (ev) => { +pageAddEventListener(mainKey, (ev) => { // 注:即使外部執行 "scriptcat-listen-inject", 不知道 inject.ts 的亂數 flag 是不可能截取資料 if (ev instanceof CustomEvent && typeof ev.detail?.injectFlagEvt === "string") { // 必定由 inject.ts 要求 @@ -122,5 +118,5 @@ scriptingMessagingBind = () => { }; // ------------ 請求 ------------ -performance.dispatchEvent(new CustomEvent(mainKey)); +pageDispatchEvent(new CustomEvent(mainKey)); // ----------------------------- diff --git a/src/inject.ts b/src/inject.ts index d7ea191a5..8eed4c49e 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -1,10 +1,7 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; -import { - CustomEventMessage, - createPageMessaging, - pageDispatchCustomEvent, -} from "@Packages/message/custom_event_message"; +import { CustomEventMessage, createPageMessaging } from "@Packages/message/custom_event_message"; +import { pageAddEventListener, pageDispatchCustomEvent } from "@Packages/message/common"; import { Server } from "@Packages/message/server"; import type { TScriptInfo } from "./app/repo/scripts"; import type { GMInfoEnv } from "./app/service/content/types"; @@ -47,7 +44,7 @@ const injectFlag = randomMessageFlag(); const injectFlagEvt = injectFlag; // 用來接收 emitter -performance.addEventListener( +pageAddEventListener( `${injectFlagEvt}`, (ev) => { if (ev instanceof CustomEvent && ev.detail?.[`emitterKeyFor${injectFlagEvt}`]) { @@ -63,7 +60,7 @@ const submitTarget = () => { }; if (submitTarget() === true) { - performance.addEventListener(mainKey, (ev) => { + pageAddEventListener(mainKey, (ev) => { if (ev instanceof CustomEvent && !ev.detail) { submitTarget(); } diff --git a/src/scripting.ts b/src/scripting.ts index 1217e923c..223bc5258 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -1,5 +1,6 @@ import { randomMessageFlag } from "./pkg/utils/utils"; -import { createPageMessaging, pageDispatchCustomEvent } from "@Packages/message/custom_event_message"; +import { createPageMessaging } from "@Packages/message/custom_event_message"; +import { pageAddEventListener, pageDispatchCustomEvent } from "@Packages/message/common"; import { uuidv5 } from "./pkg/utils/uuid"; const scriptingMessaging = createPageMessaging(""); @@ -49,7 +50,7 @@ chrome.storage.local.get(["localStorage:scriptInjectMessageFlag"]).then((m) => { const injectFlagEvt = injectFlag; // 用來接收 emitter - performance.addEventListener( + pageAddEventListener( `${injectFlagEvt}`, (ev) => { if (ev instanceof CustomEvent && ev.detail?.[`emitterKeyFor${injectFlagEvt}`]) { @@ -72,7 +73,7 @@ chrome.storage.local.get(["localStorage:scriptInjectMessageFlag"]).then((m) => { }; if (submitTarget() === true) { - performance.addEventListener(mainKey, (ev) => { + pageAddEventListener(mainKey, (ev) => { if (ev instanceof CustomEvent && !ev.detail) { submitTarget(); } From 1de8eddc5fe178fd2587b3fb1f76ed631fe3cf65 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:11:36 +0900 Subject: [PATCH 04/10] =?UTF-8?q?cloneInto=20=E6=94=B9=E7=94=A8=20?= =?UTF-8?q?=E5=9B=BA=E5=AE=9A=E7=9A=84performance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/common.ts | 3 ++- src/app/service/content/utils.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index b4757ed75..222eed3d7 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -1,6 +1,7 @@ // 避免页面载入后改动全域物件导致消息传递失败 export const MouseEventClone = MouseEvent; export const CustomEventClone = CustomEvent; +const performanceClone = performance; // 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败 export const pageDispatchEvent = performance.dispatchEvent.bind(performance); @@ -8,7 +9,7 @@ export const pageAddEventListener = performance.addEventListener.bind(performanc export const pageRemoveEventListener = performance.removeEventListener.bind(performance); const detailClone = typeof cloneInto === "function" ? cloneInto : null; export const pageDispatchCustomEvent = (eventType: string, detail: any) => { - if (detailClone && detail) detail = detailClone(detail, document.defaultView); + if (detailClone && detail) detail = detailClone(detail, performanceClone); const ev = new CustomEventClone(eventType, { detail, cancelable: true, diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index 54a2e8bed..4e9cf3f9e 100644 --- a/src/app/service/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -151,7 +151,7 @@ export function compilePreInjectScript( return `window['${flag}'] = function(){${autoDeleteMountCode}${scriptCode}}; { let o = { cancelable: true, detail: { scriptFlag: '${flag}', scriptInfo: (${scriptInfoJSON}) } }, - c = typeof cloneInto === "function" ? cloneInto(o, document.defaultView) : o, + c = typeof cloneInto === "function" ? cloneInto(o, performance) : o, f = () => performance.dispatchEvent(new CustomEvent('${evScriptLoad}', c)), needWait = f(); if (needWait) performance.addEventListener('${evEnvLoad}', f, { once: true }); From d650fdfb852f40cae877084ad549f6eb2b1ac2dd Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 11 Dec 2025 19:21:42 +0900 Subject: [PATCH 05/10] =?UTF-8?q?=E5=88=AA=E6=9C=AA=E4=BD=BF=E7=94=A8=20Im?= =?UTF-8?q?mutableEventTarget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/content.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/content.ts b/src/content.ts index e34849108..d28b314cd 100644 --- a/src/content.ts +++ b/src/content.ts @@ -19,10 +19,6 @@ const contentRandomId = uuidv4(); let scriptingMessagingBind = () => {}; // ------------ 對象 ------------ -class ImmutableEventTarget extends EventTarget {} -ImmutableEventTarget.prototype.addEventListener = EventTarget.prototype.addEventListener; -ImmutableEventTarget.prototype.dispatchEvent = EventTarget.prototype.dispatchEvent; -ImmutableEventTarget.prototype.removeEventListener = EventTarget.prototype.removeEventListener; const pageMessaging = createPageMessaging(""); const scriptExecutorPageMessaging = createPageMessaging(uuidv4()); From 78d56bce6651e1745e08a53e795f5810ff91a671 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:23:52 +0900 Subject: [PATCH 06/10] =?UTF-8?q?=E8=AF=A5=E9=94=99=E8=AF=AF=E4=B8=BA?= =?UTF-8?q?=E9=A2=84=E6=9C=9F=E5=86=85=E6=83=85=E5=86=B5=EF=BC=8C=E6=97=A0?= =?UTF-8?q?=E9=9C=80=E8=AE=B0=E5=BD=95=20debug=20=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/runtime.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 2681cdf13..4efd2057b 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -342,6 +342,8 @@ export class RuntimeService { try { const res = await chrome.userScripts.getScripts({ ids: ["scriptcat-inject"] }); registered = res.length === 1; + } catch { + // 该错误为预期内情况,无需记录 debug 日志 } finally { // 考虑 UserScripts API 不可使用等情况 runtimeGlobal.registered = registered; From 6aea588de53b5bfef0f9e7b7e4a817013b632cf3 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 27 Dec 2025 10:24:31 +0900 Subject: [PATCH 07/10] =?UTF-8?q?=E5=88=A0=E6=8E=89=E6=9C=AA=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=20getUspMessageFlag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pkg/utils/utils.ts | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index 49ce0b426..b12d7b221 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -39,31 +39,6 @@ export const deferred = (): Deferred => { return { promise, resolve, reject }; }; -export const getUspMessageFlag = () => { - const s = new Error().stack; - if (s) { - const search1 = "content.js?usp_flag="; - const len1 = search1.length; - const idx1 = s.indexOf(search1); - if (idx1 > 0) { - const search2 = "&usp_end"; - const idx2 = s.indexOf(search2, idx1 + len1); - if (idx2 > 0) { - const param = s.substring(idx1 + len1, idx2); - try { - // 使用 URLSearchParams 避免字符编码问题 - const uspString = `usp_flag=${param}`; - const usp = new URLSearchParams(uspString); - if (usp.size === 1) return usp.get("usp_flag") || null; - } catch (e) { - console.error(e); - } - } - } - } - return null; -}; - export function isFirefox() { //@ts-ignore return typeof mozInnerScreenX !== "undefined"; From a18b6d1118017f43545316d3841376ebd9583eb1 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 1 Jan 2026 07:37:56 +0900 Subject: [PATCH 08/10] =?UTF-8?q?=E9=87=8D=E6=96=B0=E6=95=B4=E7=90=86?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=EF=BC=8C=E4=BF=AE=E6=AD=A3=20`@inject-into?= =?UTF-8?q?=20content`=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/common.ts | 22 +- packages/message/custom_event_message.ts | 20 +- packages/message/server.test.ts | 40 +- rspack.config.ts | 13 +- src/app/service/content/content.ts | 149 -------- src/app/service/content/exec_script.ts | 4 +- src/app/service/content/gm_api/gm_api.test.ts | 42 +-- src/app/service/content/gm_api/gm_api.ts | 7 +- src/app/service/content/inject.ts | 92 ----- src/app/service/content/script_executor.ts | 9 +- src/app/service/content/utils.ts | 6 +- .../service/service_worker/runtime.consts.ts | 8 +- src/app/service/service_worker/runtime.ts | 5 +- src/content.ts | 246 +++++++----- src/inject.ts | 287 +++++++++++--- src/message-delivery.ts | 33 ++ src/scripting.ts | 350 +++++++++++++++--- tests/vitest.setup.ts | 13 - 18 files changed, 835 insertions(+), 511 deletions(-) delete mode 100644 src/app/service/content/content.ts delete mode 100644 src/app/service/content/inject.ts create mode 100644 src/message-delivery.ts diff --git a/packages/message/common.ts b/packages/message/common.ts index 222eed3d7..51f51b95b 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -1,12 +1,26 @@ +export const ScriptEnvTag = { + inject: "it", + content: "ct", +} as const; + +export type ScriptEnvTag = ValueOf; + +export const ScriptEnvType = { + inject: 1, + content: 2, +} as const; + +export type ScriptEnvType = ValueOf; + // 避免页面载入后改动全域物件导致消息传递失败 export const MouseEventClone = MouseEvent; export const CustomEventClone = CustomEvent; -const performanceClone = performance; +const performanceClone = process.env.VI_TESTING === "true" ? window : performance; // 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败 -export const pageDispatchEvent = performance.dispatchEvent.bind(performance); -export const pageAddEventListener = performance.addEventListener.bind(performance); -export const pageRemoveEventListener = performance.removeEventListener.bind(performance); +export const pageDispatchEvent = performanceClone.dispatchEvent.bind(performanceClone); +export const pageAddEventListener = performanceClone.addEventListener.bind(performanceClone); +export const pageRemoveEventListener = performanceClone.removeEventListener.bind(performanceClone); const detailClone = typeof cloneInto === "function" ? cloneInto : null; export const pageDispatchCustomEvent = (eventType: string, detail: any) => { if (detailClone && detail) detail = detailClone(detail, performanceClone); diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index 933681b29..55600df44 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -31,7 +31,7 @@ export class CustomEventPostMessage implements PostMessage { export type PageMessaging = { et: string; - bindEmitter?: () => void; + bindReceiver?: () => void; waitReady?: Promise; waitReadyResolve?: () => any; onReady?: (callback: () => any) => any; @@ -64,10 +64,10 @@ export class CustomEventMessage implements Message { constructor( private pageMessaging: PageMessaging, - protected readonly isContent: boolean + protected readonly isInbound: boolean ) { - this.receiveFlag = `${isContent ? DefinedFlags.contentFlag : DefinedFlags.injectFlag}${DefinedFlags.domEvent}`; - this.sendFlag = `${isContent ? DefinedFlags.injectFlag : DefinedFlags.contentFlag}${DefinedFlags.domEvent}`; + this.receiveFlag = `${isInbound ? DefinedFlags.inboundFlag : DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`; + this.sendFlag = `${isInbound ? DefinedFlags.outboundFlag : DefinedFlags.inboundFlag}${DefinedFlags.domEvent}`; this.pageMessagingHandler = (event: Event) => { if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) { relatedTargetMap.set(event.movementX, event.relatedTarget); @@ -77,10 +77,10 @@ export class CustomEventMessage implements Message { }; } - bindEmitter() { - if (!this.pageMessaging.et) throw new Error("bindEmitter() failed"); + bindReceiver() { + if (!this.pageMessaging.et) throw new Error("bindReceiver() failed"); const receiveFlag = `evt_${this.pageMessaging.et}_${this.receiveFlag}`; - pageRemoveEventListener(receiveFlag, this.pageMessagingHandler); // 避免重覆 + pageRemoveEventListener(receiveFlag, this.pageMessagingHandler); // 避免重复 pageAddEventListener(receiveFlag, this.pageMessagingHandler); } @@ -139,7 +139,7 @@ export class CustomEventMessage implements Message { } nativeSend(detail: any) { - if (!this.pageMessaging.et) throw new Error("inject.js is not ready or destroyed."); + if (!this.pageMessaging.et) throw new Error("scripting.js is not ready or destroyed."); pageDispatchCustomEvent(`evt_${this.pageMessaging.et}_${this.sendFlag}`, detail); } @@ -167,7 +167,7 @@ export class CustomEventMessage implements Message { // 与content页的消息通讯实际是同步,此方法不需要经过background // 但是请注意中间不要有promise syncSendMessage(data: TMessage): TMessage { - if (!this.pageMessaging.et) throw new Error("inject.js is not ready or destroyed."); + if (!this.pageMessaging.et) throw new Error("scripting.js is not ready or destroyed."); const messageId = uuidv4(); const body: WindowMessageBody = { messageId, @@ -187,7 +187,7 @@ export class CustomEventMessage implements Message { } sendRelatedTarget(target: EventTarget): number { - if (!this.pageMessaging.et) throw new Error("inject.js is not ready or destroyed."); + if (!this.pageMessaging.et) throw new Error("scripting.js is not ready or destroyed."); // 特殊处理relatedTarget,返回id进行关联 // 先将relatedTarget转换成id发送过去 const id = (relateId = relateId === maxInteger ? 1 : relateId + 1); diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index 6fa3e6fa2..603cb6f4e 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -5,8 +5,8 @@ import type { MessageConnect, RuntimeMessageSender } from "./types"; import { DefinedFlags } from "@App/app/service/service_worker/runtime.consts"; import { uuidv4 } from "@App/pkg/utils/uuid"; -let contentMessage: CustomEventMessage; -let injectMessage: CustomEventMessage; +let inboundMessage: CustomEventMessage; +let outboundMessage: CustomEventMessage; let server: Server; let client: CustomEventMessage; @@ -15,17 +15,17 @@ const nextTick = () => Promise.resolve().then(() => {}); const setupGlobal = () => { const testFlag = uuidv4(); const testPageMessaging = createPageMessaging(testFlag); - // 创建 content 和 inject 之间的消息通道 - contentMessage = new CustomEventMessage(testPageMessaging, true); // content 端 - injectMessage = new CustomEventMessage(testPageMessaging, false); // inject 端 - contentMessage.bindEmitter(); - injectMessage.bindEmitter(); + // 创建 scripting 和 inject / content 之间的消息通道 + inboundMessage = new CustomEventMessage(testPageMessaging, true); // scripting 端 + outboundMessage = new CustomEventMessage(testPageMessaging, false); // inject / content 端 + inboundMessage.bindReceiver(); + outboundMessage.bindReceiver(); - // 服务端使用 content 消息 - server = new Server("api", contentMessage); + // 服务端使用 scripting 消息 + server = new Server("api", inboundMessage); - // 客户端使用 inject 消息 - client = injectMessage; + // 客户端使用 inject / content 消息 + client = outboundMessage; // 清理 DOM 事件监听器 vi.stubGlobal("window", Object.create(window)); @@ -42,16 +42,16 @@ const setupGlobal = () => { let messageThis: CustomEventMessage; let messageThat: CustomEventMessage; // 根据事件类型确定目标消息处理器 - if (eventType.includes(DefinedFlags.contentFlag)) { + if (eventType.includes(DefinedFlags.inboundFlag)) { // inject -> content - targetEventType = eventType.replace(DefinedFlags.contentFlag, DefinedFlags.injectFlag); - messageThis = contentMessage; - messageThat = injectMessage; - } else if (eventType.includes(DefinedFlags.injectFlag)) { + targetEventType = eventType.replace(DefinedFlags.inboundFlag, DefinedFlags.outboundFlag); + messageThis = inboundMessage; + messageThat = outboundMessage; + } else if (eventType.includes(DefinedFlags.outboundFlag)) { // content -> inject - targetEventType = eventType.replace(DefinedFlags.injectFlag, DefinedFlags.contentFlag); - messageThis = injectMessage; - messageThat = contentMessage; + targetEventType = eventType.replace(DefinedFlags.outboundFlag, DefinedFlags.inboundFlag); + messageThis = outboundMessage; + messageThat = inboundMessage; } else { throw new Error("test mock failed"); } @@ -646,7 +646,7 @@ describe("Server", () => { }); it("应该在 enableConnect 为 false 时不处理连接", async () => { - const serverWithoutConnect = new Server("api", contentMessage, false); + const serverWithoutConnect = new Server("api", inboundMessage, false); const mockHandler = vi.fn(); serverWithoutConnect.on("on-noconnect", mockHandler); diff --git a/rspack.config.ts b/rspack.config.ts index 38ca85723..1c3b3a96d 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -19,7 +19,15 @@ const dist = path.join(dirname, "dist"); const assets = path.join(src, "assets"); // 排除这些文件,不进行分离 -const chunkExcludeSet = new Set(["editor.worker", "ts.worker", "linter.worker", "service_worker", "content", "inject"]); +const chunkExcludeSet = new Set([ + "editor.worker", + "ts.worker", + "linter.worker", + "service_worker", + "content", + "inject", + "scripting", +]); export default defineConfig({ ...(isDev @@ -112,6 +120,9 @@ export default defineConfig({ ], }, plugins: [ + new rspack.DefinePlugin({ + "process.env.VI_TESTING": "false", + }), new rspack.CopyRspackPlugin({ patterns: [ { diff --git a/src/app/service/content/content.ts b/src/app/service/content/content.ts deleted file mode 100644 index 59ed5f04e..000000000 --- a/src/app/service/content/content.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Client } from "@Packages/message/client"; -import { type CustomEventMessage } from "@Packages/message/custom_event_message"; -import { forwardMessage, type Server } from "@Packages/message/server"; -import type { MessageSend } from "@Packages/message/types"; -import type { ScriptExecutor } from "./script_executor"; -import { RuntimeClient } from "../service_worker/client"; -import { makeBlobURL } from "@App/pkg/utils/utils"; -import type { GMInfoEnv } from "./types"; -import type { Logger } from "@App/app/repo/logger"; -import LoggerCore from "@App/app/logger/core"; - -// content页的处理 -export default class ContentRuntime { - // 运行在content页面的脚本 - private readonly contentScriptSet: Set = new Set(); - - constructor( - // 监听来自service_worker的消息 - private readonly extServer: null, - // 监听来自inject的消息 - private readonly server: Server, - // 发送给扩展service_worker的通信接口 - private readonly senderToExt: MessageSend, - // 发送给inject的消息接口 - private readonly senderToInject: CustomEventMessage, - // 脚本执行器消息接口 - private readonly scriptExecutorMsg: CustomEventMessage, - private readonly scriptExecutor: ScriptExecutor - ) {} - - init() { - // this.extServer.on("runtime/emitEvent", (data) => { - // // 转发给inject和scriptExecutor - // this.scriptExecutor.emitEvent(data); - // return sendMessage(this.senderToInject, "inject/runtime/emitEvent", data); - // }); - // this.extServer.on("runtime/valueUpdate", (data) => { - // // 转发给inject和scriptExecutor - // this.scriptExecutor.valueUpdate(data); - // return sendMessage(this.senderToInject, "inject/runtime/valueUpdate", data); - // }); - this.server.on("logger", (data: Logger) => { - LoggerCore.logger().log(data.level, data.message, data.label); - }); - forwardMessage("serviceWorker", "script/isInstalled", this.server, this.senderToExt); - forwardMessage( - "serviceWorker", - "runtime/gmApi", - this.server, - this.senderToExt, - (data: { api: string; params: any; uuid: string }) => { - // 拦截关注的api - switch (data.api) { - case "CAT_createBlobUrl": { - const file = data.params[0] as File; - const url = makeBlobURL({ blob: file, persistence: false }) as string; - return url; - } - case "CAT_fetchBlob": { - return fetch(data.params[0]).then((res) => res.blob()); - } - case "CAT_fetchDocument": { - return new Promise((resolve) => { - const xhr = new XMLHttpRequest(); - xhr.responseType = "document"; - xhr.open("GET", data.params[0]); - xhr.onload = () => { - const nodeId = (this.senderToInject as CustomEventMessage).sendRelatedTarget(xhr.response); - resolve(nodeId); - }; - xhr.send(); - }); - } - case "GM_addElement": { - const [parentNodeId, tagName, tmpAttr] = data.params; - let attr = { ...tmpAttr }; - let parentNode: EventTarget | undefined; - // 判断是不是content脚本发过来的 - let msg: CustomEventMessage; - if (this.contentScriptSet.has(data.uuid) || this.scriptExecutor.execMap.has(data.uuid)) { - msg = this.scriptExecutorMsg; - } else { - msg = this.senderToInject; - } - if (parentNodeId) { - parentNode = msg.getAndDelRelatedTarget(parentNodeId); - } - const el = document.createElement(tagName); - - let textContent = ""; - if (attr) { - if (attr.textContent) { - textContent = attr.textContent; - delete attr.textContent; - } - } else { - attr = {}; - } - for (const key of Object.keys(attr)) { - el.setAttribute(key, attr[key]); - } - if (textContent) { - el.textContent = textContent; - } - (parentNode || document.head || document.body || document.querySelector("*")).appendChild(el); - const nodeId = msg.sendRelatedTarget(el); - return nodeId; - } - case "GM_log": - // 拦截GM_log,打印到控制台 - // 由于某些页面会处理掉console.log,所以丢到这里来打印 - switch (data.params.length) { - case 1: - console.log(data.params[0]); - break; - case 2: - console.log("[" + data.params[1] + "]", data.params[0]); - break; - case 3: - console.log("[" + data.params[1] + "]", data.params[0], data.params[2]); - break; - } - break; - } - return false; - } - ); - } - - pageLoad(envInfo: GMInfoEnv) { - this.scriptExecutor.checkEarlyStartScript("content", MessageFlag, envInfo); - const client = new RuntimeClient(this.senderToExt); - // 向service_worker请求脚本列表及环境信息 - client.pageLoad().then((o) => { - if (!o.ok) return; - const { injectScriptList, contentScriptList, envInfo } = o; - // 启动脚本:向 inject页面 发送脚本列表及环境信息 - const client = new Client(this.senderToInject, "inject"); - // 根据@inject-into content过滤脚本 - client.do("pageLoad", { injectScriptList, envInfo }); - // 处理注入到content环境的脚本 - for (const script of contentScriptList) { - this.contentScriptSet.add(script.uuid); - } - // 启动脚本 - this.scriptExecutor.startScripts(contentScriptList, envInfo); - }); - } -} diff --git a/src/app/service/content/exec_script.ts b/src/app/service/content/exec_script.ts index 9d54bf883..dcdb99259 100644 --- a/src/app/service/content/exec_script.ts +++ b/src/app/service/content/exec_script.ts @@ -25,7 +25,7 @@ export default class ExecScript { constructor( scriptRes: TScriptInfo, - envPrefix: "content" | "offscreen", + envPrefix: "scripting" | "offscreen", message: Message, code: string | ScriptFunc, envInfo: GMInfoEnv, @@ -48,7 +48,7 @@ export default class ExecScript { if (grantSet.has("none")) { // 不注入任何GM api // ScriptCat行为:GM.info 和 GM_info 同时注入 - // 不改变Context情况下,以 named 传多於一个全域变量 + // 不改变Context情况下,以 named 传多于一个全域变量 this.named = { GM: { info: GM_info }, GM_info }; } else { // 构建脚本GM上下文 diff --git a/src/app/service/content/gm_api/gm_api.test.ts b/src/app/service/content/gm_api/gm_api.test.ts index 28c46ee12..9a97c1b3e 100644 --- a/src/app/service/content/gm_api/gm_api.test.ts +++ b/src/app/service/content/gm_api/gm_api.test.ts @@ -264,7 +264,7 @@ describe.concurrent("GM_menu", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); @@ -278,7 +278,7 @@ describe.concurrent("GM_menu", () => { expect(mockSendMessage).toHaveBeenCalledWith( expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_registerMenuCommand", params: [ @@ -314,7 +314,7 @@ describe.concurrent("GM_menu", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = exec.exec(); // 验证 sendMessage 是否被调用 @@ -336,7 +336,7 @@ describe.concurrent("GM_menu", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); @@ -350,7 +350,7 @@ describe.concurrent("GM_menu", () => { expect(mockSendMessage).toHaveBeenCalledWith( expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_registerMenuCommand", params: [ @@ -399,7 +399,7 @@ describe.concurrent("GM_menu", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual({ id1: "abc", id2: "abc", id3: 1, id4: 2, id5: "3", id6: 3, id7: 3, id8: 4 }); @@ -423,7 +423,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -434,7 +434,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValue", params: [expect.any(String), "a", 123], @@ -448,7 +448,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValue", params: [expect.any(String), "a"], @@ -477,7 +477,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -494,7 +494,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValues", params: [ @@ -518,7 +518,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValues", params: [ @@ -552,7 +552,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -568,7 +568,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValues", params: [ @@ -587,7 +587,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValue", params: [ @@ -621,7 +621,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -638,7 +638,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValues", params: [ @@ -662,7 +662,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValues", params: [ @@ -697,7 +697,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); expect(mockSendMessage).toHaveBeenCalledTimes(1); @@ -731,7 +731,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); // remote = true const retPromise = exec.exec(); @@ -757,7 +757,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index b8d0f0ce7..f46d76976 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -37,6 +37,9 @@ export interface GMRequestHandle { abort: () => void; } +// 判断当前是否运行在 USER_SCRIPT 环境 (content环境) +const isContent = typeof chrome.runtime?.sendMessage === "function"; + const integrity = {}; // 仅防止非法实例化 let valChangeCounterId = 0; @@ -706,7 +709,7 @@ export default class GMApi extends GM_Base { parentNodeId = id; } else { parentNodeId = null; - attrs = tagName as Record; + attrs = (tagName || {}) as Record; tagName = parentNode as string; } if (typeof tagName !== "string") throw new Error("The parameter 'tagName' of GM_addElement shall be a string."); @@ -716,7 +719,7 @@ export default class GMApi extends GM_Base { data: { uuid: this.scriptRes.uuid, api: "GM_addElement", - params: [parentNodeId, tagName, attrs], + params: [parentNodeId, tagName, attrs, isContent], }, }); if (resp.code) { diff --git a/src/app/service/content/inject.ts b/src/app/service/content/inject.ts deleted file mode 100644 index a503529bb..000000000 --- a/src/app/service/content/inject.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { type Server } from "@Packages/message/server"; -import type { Message } from "@Packages/message/types"; -import { ExternalWhitelist } from "@App/app/const"; -import { sendMessage } from "@Packages/message/client"; -import type { ScriptExecutor } from "./script_executor"; -import type { TScriptInfo } from "@App/app/repo/scripts"; -import type { EmitEventRequest } from "../service_worker/types"; -import type { GMInfoEnv, ValueUpdateDataEncoded } from "./types"; - -export class InjectRuntime { - constructor( - private readonly server: Server, - private readonly msg: Message, - private readonly scriptExecutor: ScriptExecutor - ) {} - - init() { - this.server.on("runtime/emitEvent", (data: EmitEventRequest) => { - // 转发给脚本 - this.scriptExecutor.emitEvent(data); - }); - this.server.on("runtime/valueUpdate", (data: ValueUpdateDataEncoded) => { - this.scriptExecutor.valueUpdate(data); - }); - } - - startScripts(injectScriptList: TScriptInfo[], envInfo: GMInfoEnv) { - this.scriptExecutor.startScripts(injectScriptList, envInfo); - } - - onInjectPageLoaded() { - // 注入允许外部调用 - this.externalMessage(); - } - - externalMessage() { - // 对外接口白名单 - const hostname = window.location.hostname; - if ( - ExternalWhitelist.some( - // 如果当前页面的 hostname 是白名单的网域或其子网域 - (t) => hostname.endsWith(t) && (hostname.length === t.length || hostname.endsWith(`.${t}`)) - ) - ) { - const msg = this.msg; - // 注入 - const external: External = window.external || (window.external = {} as External); - const scriptExpose: App.ExternalScriptCat = { - isInstalled(name: string, namespace: string, callback: (res: App.IsInstalledResponse | undefined) => unknown) { - sendMessage(msg, "content/script/isInstalled", { - name, - namespace, - }).then(callback); - }, - }; - try { - external.Scriptcat = scriptExpose; - } catch { - // 无法注入到 external,忽略 - } - const exposedTM = external.Tampermonkey; - const isInstalledTM = exposedTM?.isInstalled; - const isInstalledSC = scriptExpose.isInstalled; - if (isInstalledTM && exposedTM?.getVersion && exposedTM.openOptions) { - // 当TM和SC同时启动的特殊处理:如TM没有安装,则查SC的安装状态 - try { - exposedTM.isInstalled = ( - name: string, - namespace: string, - callback: (res: App.IsInstalledResponse | undefined) => unknown - ) => { - isInstalledTM(name, namespace, (res) => { - if (res?.installed) callback(res); - else - isInstalledSC(name, namespace, (res) => { - callback(res); - }); - }); - }; - } catch { - // 忽略错误 - } - } else { - try { - external.Tampermonkey = scriptExpose; - } catch { - // 无法注入到 external,忽略 - } - } - } - } -} diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index de6d2ed28..181bbb062 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -6,7 +6,7 @@ import type { GMInfoEnv, ScriptFunc, ValueUpdateDataEncoded } from "./types"; import { addStyleSheet, definePropertyListener } from "./utils"; import type { ScriptLoadInfo, TScriptInfo } from "@App/app/repo/scripts"; import { DefinedFlags } from "../service_worker/runtime.consts"; -import { pageDispatchEvent } from "@Packages/message/common"; +import { pageDispatchEvent, type ScriptEnvTag } from "@Packages/message/common"; import { isUrlExcluded } from "@App/pkg/utils/match"; export type ExecScriptEntry = { @@ -85,9 +85,8 @@ export class ScriptExecutor { }); } - checkEarlyStartScript(env: "content" | "inject", messageFlag: string, envInfo: GMInfoEnv) { - const isContent = env === "content"; - const eventNamePrefix = `evt${messageFlag}${isContent ? DefinedFlags.contentFlag : DefinedFlags.injectFlag}`; + checkEarlyStartScript(scriptEnvTag: ScriptEnvTag, messageFlag: string, envInfo: GMInfoEnv) { + const eventNamePrefix = `evt${messageFlag}.${scriptEnvTag}`; const scriptLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.scriptLoadComplete}`; const envLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.envLoadComplete}`; // 监听 脚本加载 @@ -138,7 +137,7 @@ export class ScriptExecutor { execScriptEntry(scriptEntry: ExecScriptEntry) { const { scriptLoadInfo, scriptFunc, envInfo } = scriptEntry; - const exec = new ExecScript(scriptLoadInfo, "content", this.msg, scriptFunc, envInfo); + const exec = new ExecScript(scriptLoadInfo, "scripting", this.msg, scriptFunc, envInfo); this.execMap.set(scriptLoadInfo.uuid, exec); const metadata = scriptLoadInfo.metadata || {}; const resource = scriptLoadInfo.resource; diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index 4e9cf3f9e..647fc028a 100644 --- a/src/app/service/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -3,6 +3,7 @@ import type { ScriptFunc } from "./types"; import type { ScriptLoadInfo } from "../service_worker/types"; import { DefinedFlags } from "../service_worker/runtime.consts"; import { sourceMapTo } from "@App/pkg/utils/utils"; +import { ScriptEnvTag } from "@Packages/message/common"; export type CompileScriptCodeResource = { name: string; @@ -139,9 +140,8 @@ export function compilePreInjectScript( scriptCode: string, autoDeleteMountFunction: boolean = false ): string { - const eventNamePrefix = `evt${messageFlag}${ - isInjectIntoContent(script.metadata) ? DefinedFlags.contentFlag : DefinedFlags.injectFlag - }`; + const scriptEnvTag = isInjectIntoContent(script.metadata) ? ScriptEnvTag.content : ScriptEnvTag.inject; + const eventNamePrefix = `evt${messageFlag}.${scriptEnvTag}`; const flag = `${script.flag}`; const scriptInfo = trimScriptInfo(script); const scriptInfoJSON = `${JSON.stringify(scriptInfo)}`; diff --git a/src/app/service/service_worker/runtime.consts.ts b/src/app/service/service_worker/runtime.consts.ts index 3e8bcbf3a..0616566b4 100644 --- a/src/app/service/service_worker/runtime.consts.ts +++ b/src/app/service/service_worker/runtime.consts.ts @@ -1,8 +1,8 @@ export const DefinedFlags = { - // content 环境flag - contentFlag: ".ct", - // inject 环境flag - injectFlag: ".fd", + // Server: 回应 outbound (scripting -> content / page) + inboundFlag: ".ib", + // Client: 发送至 inbound (content / page -> scripting) + outboundFlag: ".ob", // 脚本加载完成事件 scriptLoadComplete: ".slc", // 环境加载完成事件 diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 93b57c979..bab1685d7 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -904,6 +904,7 @@ export class RuntimeService { // scriptcat-content/scriptcat-inject不存在的情况 // 走一次重新注册的流程 this.logger.warn("registered = true but scriptcat-content/scriptcat-inject not exists, re-register userscripts."); + runtimeGlobal.registered = false; // 异常时强制反注册 } // 删除旧注册 await this.unregisterUserscripts(); @@ -969,7 +970,7 @@ export class RuntimeService { documentId: to.documentId, frameId: to.frameId, }), - "content/runtime/" + action, + "scripting/runtime/" + action, data ); } @@ -985,7 +986,7 @@ export class RuntimeService { documentId: to.documentId, frameId: to.frameId, }), - "content/runtime/emitEvent", + "scripting/runtime/emitEvent", req ); } diff --git a/src/content.ts b/src/content.ts index d28b314cd..ae1d7afc0 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,118 +1,182 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; -import { ExtensionMessage } from "@Packages/message/extension_message"; import { CustomEventMessage, createPageMessaging } from "@Packages/message/custom_event_message"; import { pageAddEventListener, pageDispatchCustomEvent, pageDispatchEvent } from "@Packages/message/common"; -import { Server } from "@Packages/message/server"; -import ContentRuntime from "./app/service/content/content"; +import { ScriptEnvTag } from "@Packages/message/common"; +import { uuidv5 } from "./pkg/utils/uuid"; import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_executor"; -import type { Message } from "@Packages/message/types"; -import { sendMessage } from "@Packages/message/client"; import type { ValueUpdateDataEncoded } from "./app/service/content/types"; -import { uuidv4, uuidv5 } from "./pkg/utils/uuid"; +import type { TClientPageLoadInfo } from "./app/repo/scripts"; /* global MessageFlag */ -const mainKey = uuidv5("scriptcat-listen-inject", MessageFlag); +// ================================ +// 常量与全局状态 +// ================================ -const contentRandomId = uuidv4(); +// 判断当前是否运行在 USER_SCRIPT 环境 (content环境) +const isContent = typeof chrome.runtime?.sendMessage === "function"; +const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; -let scriptingMessagingBind = () => {}; -// ------------ 對象 ------------ +// 用于通知页面:content executor 已准备好 +const executorEnvReadyKey = uuidv5("scriptcat-executor-ready", MessageFlag); -const pageMessaging = createPageMessaging(""); -const scriptExecutorPageMessaging = createPageMessaging(uuidv4()); +// 页面通信通道(event token 会在握手后设置) +const scriptingMessaging = createPageMessaging(""); // injectFlagEvt +const pageMessaging = createPageMessaging(""); // `${injectFlagEvt}_${scriptEnvTag}` -const scriptingMessaging = createPageMessaging(""); +// scripting <-> content 的双向消息桥 +const msg = new CustomEventMessage(pageMessaging, false); -const emitters = new Map(); +// 日志系统(仅在 scripting 环境打印) +const logger = new LoggerCore({ + writer: new MessageWriter(msg, "scripting/logger"), + consoleLevel: "none", + labels: { env: "content", href: window.location.href }, +}); + +// 脚本执行器 +const scriptExecutor = new ScriptExecutor(msg); + +// 一次性绑定函数(绑定完成后会被置空) +let bindScriptingDeliveryOnce: (() => void) | null = null; + +// ================================ +// 工具函数:token 与握手 +// ================================ + +// 确保 scripting messaging 已就绪 +const requireScriptingToken = (): string => { + if (!scriptingMessaging.et) { + // scriptingMessaging 尚未准备好或已被销毁 + throw new Error("scriptingMessaging is not ready or destroyed"); + } + return scriptingMessaging.et; +}; -const msgInject = new CustomEventMessage(pageMessaging, true); +// 重置所有页面通信 token(用于反注册脚本) +const resetMessagingTokens = () => { + scriptingMessaging.et = ""; + pageMessaging.et = ""; +}; -// ------------ 監聽 ------------ +// 根据 injectFlagEvt 设置双方通信 token +const setMessagingTokens = (injectFlagEvt: string) => { + scriptingMessaging.et = injectFlagEvt; + pageMessaging.et = `${injectFlagEvt}_${scriptEnvTag}`; +}; -pageAddEventListener(mainKey, (ev) => { - // 注:即使外部執行 "scriptcat-listen-inject", 不知道 inject.ts 的亂數 flag 是不可能截取資料 - if (ev instanceof CustomEvent && typeof ev.detail?.injectFlagEvt === "string") { - // 必定由 inject.ts 要求 - ev.preventDefault(); // dispatchEvent 返回 false - // 按 inject.ts 要求返回 emitter - const { injectFlagEvt, scripting } = ev.detail; - let emitter = emitters.get(injectFlagEvt); - if (!emitter) { - emitters.set(injectFlagEvt, (emitter = uuidv5(injectFlagEvt, contentRandomId))); +// 通知 scripting 侧:content 已完成初始化 +const acknowledgeScriptingReady = (injectFlagEvt: string) => { + pageDispatchCustomEvent(injectFlagEvt, { + [`emitterKeyFor${injectFlagEvt}`]: isContent ? 2 : 1, + }); +}; + +// ================================ +// 消息分发处理 +// ================================ + +// 处理 scripting -> content 的消息 +const handleDeliveryMessage = (tag: string, value: any) => { + switch (tag) { + case "localStorage:scriptInjectMessageFlag": { + // 反注册所有脚本时,中断页面通信 + resetMessagingTokens(); + return; } - if (scripting) { - scriptingMessaging.et = emitter; - scriptingMessagingBind(); - } else { - pageMessaging.et = emitter; - msgInject.bindEmitter(); + + case "valueUpdateDelivery": { + // storage / value 更新同步 + const sendData = value.sendData as ValueUpdateDataEncoded; + scriptExecutor.valueUpdate(sendData); + return; + } + + case "scripting/runtime/emitEvent": { + // scripting 主动触发事件 + scriptExecutor.emitEvent(value); + return; } - // 傳送 emitter 給 inject.ts - pageDispatchCustomEvent(`${injectFlagEvt}`, { - [`emitterKeyFor${injectFlagEvt}`]: emitter, - }); + + case "pageLoad": { + // 页面加载完成,启动匹配的脚本 + const info = value as TClientPageLoadInfo; + if (!info.ok) return; + + const { contentScriptList, envInfo } = info; + logger.logger().debug("content start - pageload"); + scriptExecutor.startScripts(contentScriptList, envInfo); + return; + } + + default: + // 未识别的消息类型直接忽略 + return; } -}); +}; -// ------------ 连接 ------------ +// ================================ +// 页面通信绑定与握手 +// ================================ -// 建立与service_worker页面的连接 -const extMsgComm: Message = new ExtensionMessage(false); -// 初始化日志组件 -const loggerCore = new LoggerCore({ - writer: new MessageWriter(extMsgComm, "serviceWorker/logger"), - labels: { env: "content" }, -}); +// 监听 scripting 发来的 delivery 消息 +const bindScriptingDeliveryChannel = () => { + const token = requireScriptingToken(); + + pageAddEventListener(`evt_${token}_deliveryMessage`, (ev) => { + if (!(ev instanceof CustomEvent)) return; + + const { tag, value } = ev.detail ?? {}; + handleDeliveryMessage(tag, value); + }); +}; + +// 建立 scripting <-> content 的握手流程 +const setupHandshake = () => { + // 准备一次性绑定函数 + bindScriptingDeliveryOnce = () => { + bindScriptingDeliveryOnce = null; + bindScriptingDeliveryChannel(); + }; + + // 等待 scripting 注入完成并发送 injectFlagEvt + pageAddEventListener(executorEnvReadyKey, (ev) => { + if (!(ev instanceof CustomEvent)) return; -loggerCore.logger().debug("content start"); - -// 处理scriptExecutor -const scriptExecutorMsg1 = new CustomEventMessage(scriptExecutorPageMessaging, true); -scriptExecutorMsg1.bindEmitter(); -const scriptExecutorMsg2 = new CustomEventMessage(scriptExecutorPageMessaging, false); -scriptExecutorMsg2.bindEmitter(); -const scriptExecutor = new ScriptExecutor(scriptExecutorMsg2); - -const server = new Server("content", [msgInject, scriptExecutorMsg1]); - -// Opera中没有chrome.runtime.onConnect,并且content也不需要chrome.runtime.onConnect -// 所以不需要处理连接,设置为false -// const extServer = new Server("content", extMsgComm, false); -// scriptExecutor的消息接口 -// 初始化运行环境 -const runtime = new ContentRuntime(null, server, extMsgComm, msgInject, scriptExecutorMsg1, scriptExecutor); -runtime.init(); -// 页面加载,注入脚本 -runtime.pageLoad(initEnvInfo); - -scriptingMessagingBind = () => { - if (!scriptingMessaging.et) throw new Error("scriptingMessaging is not ready or destroyed"); - pageAddEventListener(`evt_${scriptingMessaging.et}_deliveryMessage`, (ev) => { - if (ev instanceof CustomEvent) { - const { tag, value } = ev.detail; - if (tag === "localStorage:scriptInjectMessageFlag") { - // 反注册所有脚本时,同时中断网页信息传递 - pageMessaging.et = ""; - scriptExecutorPageMessaging.et = ""; - scriptingMessaging.et = ""; - } else if (tag === "valueUpdateDelivery") { - // const storageName = sendData.storageName; - // 转发给inject和scriptExecutor - const sendData = value.sendData as ValueUpdateDataEncoded; - scriptExecutor.valueUpdate(sendData); - sendMessage(msgInject, "inject/runtime/valueUpdate", sendData); - } else if (tag === "content/runtime/emitEvent") { - const data = value; - // 转发给inject和scriptExecutor - scriptExecutor.emitEvent(data); - sendMessage(msgInject, "inject/runtime/emitEvent", data); - } + const injectFlagEvt = ev.detail?.injectFlagEvt; + + // 已初始化 / 参数非法 / 已绑定过 → 忽略 + if (scriptingMessaging.et || typeof injectFlagEvt !== "string" || !bindScriptingDeliveryOnce) { + return; } + + // 接受此次握手 + ev.preventDefault(); + + // 初始化通信 token + setMessagingTokens(injectFlagEvt); + msg.bindReceiver(); + + logger.logger().debug("content start - init"); + + // 建立消息监听 + bindScriptingDeliveryOnce(); + + // 回传 ready 信号 + acknowledgeScriptingReady(injectFlagEvt); }); }; -// ------------ 請求 ------------ -pageDispatchEvent(new CustomEvent(mainKey)); -// ----------------------------- +// ================================ +// 启动流程 +// ================================ + +// 检查 early-start 脚本 +scriptExecutor.checkEarlyStartScript(scriptEnvTag, MessageFlag, initEnvInfo); + +// 建立握手与通信绑定 +setupHandshake(); + +// 主动触发 ready 事件,请求 scripting 建立连接 +pageDispatchEvent(new CustomEvent(executorEnvReadyKey)); diff --git a/src/inject.ts b/src/inject.ts index 8eed4c49e..a26154690 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -1,68 +1,271 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; import { CustomEventMessage, createPageMessaging } from "@Packages/message/custom_event_message"; -import { pageAddEventListener, pageDispatchCustomEvent } from "@Packages/message/common"; -import { Server } from "@Packages/message/server"; -import type { TScriptInfo } from "./app/repo/scripts"; -import type { GMInfoEnv } from "./app/service/content/types"; -import { InjectRuntime } from "./app/service/content/inject"; -import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_executor"; -import { randomMessageFlag } from "./pkg/utils/utils"; +import { pageAddEventListener, pageDispatchCustomEvent, pageDispatchEvent } from "@Packages/message/common"; +import { ScriptEnvTag } from "@Packages/message/common"; import { uuidv5 } from "./pkg/utils/uuid"; +import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_executor"; +import type { ValueUpdateDataEncoded } from "./app/service/content/types"; +import type { TClientPageLoadInfo } from "./app/repo/scripts"; +import type { Message } from "@Packages/message/types"; +import { sendMessage } from "@Packages/message/client"; +import { ExternalWhitelist } from "@App/app/const"; /* global MessageFlag */ -const mainKey = uuidv5("scriptcat-listen-inject", MessageFlag); +// ================================ +// 常量与全局状态 +// ================================ + +// 判断当前是否运行在 USER_SCRIPT 环境 (content环境) +const isContent = typeof chrome.runtime?.sendMessage === "function"; +const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; + +// 用于通知页面:inject executor 已准备好 +const executorEnvReadyKey = uuidv5("scriptcat-executor-ready", MessageFlag); -const pageMessaging = createPageMessaging(""); +// 页面通信通道(event token 会在握手后设置) +const scriptingMessaging = createPageMessaging(""); // injectFlagEvt +const pageMessaging = createPageMessaging(""); // `${injectFlagEvt}_${scriptEnvTag}` +// scripting <-> inject 的双向消息桥 const msg = new CustomEventMessage(pageMessaging, false); -// 加载logger组件 +// 日志系统(仅在 scripting 环境打印) const logger = new LoggerCore({ - writer: new MessageWriter(msg, "content/logger"), - consoleLevel: "none", // 只让日志在content环境中打印 + writer: new MessageWriter(msg, "scripting/logger"), + consoleLevel: "none", labels: { env: "inject", href: window.location.href }, }); -const server = new Server("inject", msg); +// 脚本执行器 const scriptExecutor = new ScriptExecutor(msg); -const runtime = new InjectRuntime(server, msg, scriptExecutor); -runtime.init(); -// 检查early-start的脚本 -scriptExecutor.checkEarlyStartScript("inject", MessageFlag, initEnvInfo); +// 一次性绑定函数(绑定完成后会被置空) +let bindScriptingDeliveryOnce: (() => void) | null = null; -server.on("pageLoad", (data: { injectScriptList: TScriptInfo[]; envInfo: GMInfoEnv }) => { - logger.logger().debug("inject start"); - // 监听事件 - runtime.startScripts(data.injectScriptList, data.envInfo); - runtime.onInjectPageLoaded(); -}); +// ================================ +// 工具函数:token 与握手 +// ================================ + +// 确保 scripting messaging 已就绪 +const requireScriptingToken = (): string => { + if (!scriptingMessaging.et) { + // scriptingMessaging 尚未准备好或已被销毁 + throw new Error("scriptingMessaging is not ready or destroyed"); + } + return scriptingMessaging.et; +}; + +// 重置所有页面通信 token(用于反注册脚本) +const resetMessagingTokens = () => { + scriptingMessaging.et = ""; + pageMessaging.et = ""; +}; + +// 根据 injectFlagEvt 设置双方通信 token +const setMessagingTokens = (injectFlagEvt: string) => { + scriptingMessaging.et = injectFlagEvt; + pageMessaging.et = `${injectFlagEvt}_${scriptEnvTag}`; +}; + +// 通知 scripting 侧:inject 已完成初始化 +const acknowledgeScriptingReady = (injectFlagEvt: string) => { + pageDispatchCustomEvent(injectFlagEvt, { + [`emitterKeyFor${injectFlagEvt}`]: isContent ? 2 : 1, + }); +}; -const injectFlag = randomMessageFlag(); -const injectFlagEvt = injectFlag; +// ================================ +// 对外接口:external 注入 +// ================================ + +// 判断当前 hostname 是否命中白名单(含子域名) +function isExternalWhitelisted(hostname: string) { + return ExternalWhitelist.some( + (t) => hostname.endsWith(t) && (hostname.length === t.length || hostname.endsWith(`.${t}`)) + ); +} -// 用來接收 emitter -pageAddEventListener( - `${injectFlagEvt}`, - (ev) => { - if (ev instanceof CustomEvent && ev.detail?.[`emitterKeyFor${injectFlagEvt}`]) { - pageMessaging.et = ev.detail[`emitterKeyFor${injectFlagEvt}`]; - msg.bindEmitter(); +// 生成暴露给页面的 Scriptcat 外部接口 +function createScriptcatExpose(pageMsg: Message) { + const scriptExpose: App.ExternalScriptCat = { + isInstalled(name: string, namespace: string, callback: (res: App.IsInstalledResponse | undefined) => unknown) { + sendMessage(pageMsg, "scripting/script/isInstalled", { name, namespace }).then(callback); + }, + }; + return scriptExpose; +} + +// 尝试写入 external,失败则忽略 +function safeSetExternal(external: any, key: string, value: T) { + try { + external[key] = value; + return true; + } catch { + // 无法注入到 external,忽略 + return false; + } +} + +// 当 TM 与 SC 同时存在时的兼容处理:TM 未安装脚本时回退查询 SC +function patchTampermonkeyIsInstalled(external: any, scriptExpose: App.ExternalScriptCat) { + const exposedTM = external.Tampermonkey; + const isInstalledTM = exposedTM?.isInstalled; + const isInstalledSC = scriptExpose.isInstalled; + + // 满足这些字段时,认为是较完整的 TM 对象 + if (isInstalledTM && exposedTM?.getVersion && exposedTM.openOptions) { + try { + exposedTM.isInstalled = ( + name: string, + namespace: string, + callback: (res: App.IsInstalledResponse | undefined) => unknown + ) => { + isInstalledTM(name, namespace, (res: App.IsInstalledResponse | undefined) => { + if (res?.installed) callback(res); + else isInstalledSC(name, namespace, callback); + }); + }; + } catch { + // 忽略错误 } - }, - { once: true } -); + return true; + } + + return false; +} + +// inject 环境 pageLoad 后执行:按白名单对页面注入 external 接口 +function onInjectPageLoaded(pageMsg: Message) { + const hostname = window.location.hostname; + + // 不在白名单则不对外暴露接口 + if (!isExternalWhitelisted(hostname)) return; + + // 确保 external 存在 + const external: External = (window.external || (window.external = {} as External)) as External; + + // 创建 Scriptcat 暴露对象 + const scriptExpose = createScriptcatExpose(pageMsg); + + // 尝试设置 external.Scriptcat + safeSetExternal(external, "Scriptcat", scriptExpose); + + // 如果页面已有 Tampermonkey,则做兼容补丁;否则将 Tampermonkey 也指向 Scriptcat 接口 + const patched = patchTampermonkeyIsInstalled(external, scriptExpose); + if (!patched) { + safeSetExternal(external, "Tampermonkey", scriptExpose); + } +} + +// ================================ +// 消息分发处理 +// ================================ -const submitTarget = () => { - return pageDispatchCustomEvent(mainKey, { injectFlagEvt }); +// 处理 scripting -> inject 的消息 +const handleDeliveryMessage = (tag: string, value: any) => { + switch (tag) { + case "localStorage:scriptInjectMessageFlag": { + // 反注册所有脚本时,中断页面通信 + resetMessagingTokens(); + return; + } + + case "valueUpdateDelivery": { + // storage / value 更新同步 + const sendData = value.sendData as ValueUpdateDataEncoded; + scriptExecutor.valueUpdate(sendData); + return; + } + + case "scripting/runtime/emitEvent": { + // scripting 主动触发事件 + scriptExecutor.emitEvent(value); + return; + } + + case "pageLoad": { + // 页面加载完成,启动匹配的脚本,并在需要时注入 external + const info = value as TClientPageLoadInfo; + if (!info.ok) return; + + const { injectScriptList, envInfo } = info; + logger.logger().debug("inject start - pageload"); + scriptExecutor.startScripts(injectScriptList, envInfo); + + // pageLoad 后再做 external 注入,避免过早修改页面对象 + onInjectPageLoaded(msg); + return; + } + + default: + // 未识别的消息类型直接忽略 + return; + } +}; + +// ================================ +// 页面通信绑定与握手 +// ================================ + +// 监听 scripting 发来的 delivery 消息 +const bindScriptingDeliveryChannel = () => { + const token = requireScriptingToken(); + + pageAddEventListener(`evt_${token}_deliveryMessage`, (ev) => { + if (!(ev instanceof CustomEvent)) return; + + const { tag, value } = ev.detail ?? {}; + handleDeliveryMessage(tag, value); + }); }; -if (submitTarget() === true) { - pageAddEventListener(mainKey, (ev) => { - if (ev instanceof CustomEvent && !ev.detail) { - submitTarget(); +// 建立 scripting <-> inject 的握手流程 +const setupHandshake = () => { + // 准备一次性绑定函数 + bindScriptingDeliveryOnce = () => { + bindScriptingDeliveryOnce = null; + bindScriptingDeliveryChannel(); + }; + + // 等待 scripting 注入完成并发送 injectFlagEvt + pageAddEventListener(executorEnvReadyKey, (ev) => { + if (!(ev instanceof CustomEvent)) return; + + const injectFlagEvt = ev.detail?.injectFlagEvt; + + // 已初始化 / 参数非法 / 已绑定过 → 忽略 + if (scriptingMessaging.et || typeof injectFlagEvt !== "string" || !bindScriptingDeliveryOnce) { + return; } + + // 接受此次握手 + ev.preventDefault(); + + // 初始化通信 token + setMessagingTokens(injectFlagEvt); + msg.bindReceiver(); + + logger.logger().debug("inject start - init"); + + // 建立消息监听 + bindScriptingDeliveryOnce(); + + // 回传 ready 信号 + acknowledgeScriptingReady(injectFlagEvt); }); -} +}; + +// ================================ +// 启动流程 +// ================================ + +// 检查 early-start 脚本 +scriptExecutor.checkEarlyStartScript(scriptEnvTag, MessageFlag, initEnvInfo); + +// 建立握手与通信绑定 +setupHandshake(); + +// 主动触发 ready 事件,请求 scripting 建立连接 +pageDispatchEvent(new CustomEvent(executorEnvReadyKey)); diff --git a/src/message-delivery.ts b/src/message-delivery.ts new file mode 100644 index 000000000..6cb877bad --- /dev/null +++ b/src/message-delivery.ts @@ -0,0 +1,33 @@ +import { pageDispatchCustomEvent } from "@Packages/message/common"; + +export class MessageDelivery { + private messageStack: any[] | null = []; + private messageKey: string = ""; + + public dispatch(detail: any) { + const messageStack = this.messageStack; + const messageKey = this.messageKey; + if (messageStack === null) { + if (!messageKey) throw new Error("messageKey is not ready or destroyed"); + pageDispatchCustomEvent(messageKey, detail); + } else { + // 在取得 messageKey 前,先堆叠一下,避免漏掉 + messageStack.push(detail); + } + } + + public setup(et: string) { + this.messageKey = `${et}`; + const messageStack = this.messageStack; + if (messageStack) { + const messages = messageStack.slice(); + messageStack.length = 0; + this.messageStack = null; + if (messages.length > 0) { + for (const message of messages) { + this.dispatch(message); + } + } + } + } +} diff --git a/src/scripting.ts b/src/scripting.ts index 223bc5258..255cd77c2 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -1,82 +1,332 @@ -import { randomMessageFlag } from "./pkg/utils/utils"; -import { createPageMessaging } from "@Packages/message/custom_event_message"; +import LoggerCore from "./app/logger/core"; +import MessageWriter from "./app/logger/message_writer"; +import { CustomEventMessage, createPageMessaging } from "@Packages/message/custom_event_message"; import { pageAddEventListener, pageDispatchCustomEvent } from "@Packages/message/common"; +import { ScriptEnvTag, ScriptEnvType } from "@Packages/message/common"; import { uuidv5 } from "./pkg/utils/uuid"; +import { randomMessageFlag, makeBlobURL } from "@App/pkg/utils/utils"; +import { ExtensionMessage } from "@Packages/message/extension_message"; +import type { Message, MessageSend } from "@Packages/message/types"; +import { Server, forwardMessage } from "@Packages/message/server"; +import { RuntimeClient } from "@App/app/service/service_worker/client"; +import type { Logger } from "@App/app/repo/logger"; +import { MessageDelivery } from "./message-delivery"; -const scriptingMessaging = createPageMessaging(""); -const messageStack: any[] = []; +// ================================ +// 常量与全局状态 +// ================================ -// 在取得 scriptInjectMessageFlag 前,先堆叠一下,避免漏掉 -let dispatchDeliveryMessage = (message: any) => { - messageStack.push(message); +// 记录脚本 uuid 来自 inject(1) / content(2) +const uuids = new Map(); + +// 与 service_worker 通信的 sender(scripting -> service_worker) +const senderToExt: Message = new ExtensionMessage(false); + +// scripting <-> inject/content 的 page messaging(token 在握手后设置) +const scriptExecutorMsgIT = createPageMessaging(""); +const scriptExecutorMsgCT = createPageMessaging(""); + +// scripting <-> inject/content 的双向消息桥 +const scriptExecutorMsgTxIT = new CustomEventMessage(scriptExecutorMsgIT, true); // 双向:scripting <-> inject +const scriptExecutorMsgTxCT = new CustomEventMessage(scriptExecutorMsgCT, true); // 双向:scripting <-> content + +// 初始化日志组件(写入 service_worker/logger) +const loggerCore = new LoggerCore({ + writer: new MessageWriter(senderToExt, "serviceWorker/logger"), + labels: { env: "scripting" }, +}); + +// scripting 对页面投递消息的通道(token 在握手后设置) +const scriptingMessaging = createPageMessaging(""); // 对 inject / content 的 client 发出消息 + +// 将消息从 scripting 投递到 inject/content 的工具(基于自定义事件) +const messageDeliveryToPage = new MessageDelivery(); + +// service_worker 客户端 +const client = new RuntimeClient(senderToExt); + +loggerCore.logger().debug("scripting start"); + +// ================================ +// 工具函数:基础检查与小封装 +// ================================ + +// 确保 scripting messaging 已就绪 +const requireScriptingToken = (): string => { + if (!scriptingMessaging.et) { + // scriptingMessaging 尚未准备好或已被销毁 + throw new Error("scriptingMessaging is not ready or destroyed"); + } + return scriptingMessaging.et; +}; + +const setupDeliveryChannel = () => { + const token = requireScriptingToken(); + messageDeliveryToPage.setup(`evt_${token}_deliveryMessage`); +}; + +// ================================ +// 页面消息监听(调试用) +// ================================ + +// 页面消息监听(TBC) +const listenPageMessages = () => { + const token = requireScriptingToken(); + pageAddEventListener(`evt_${token}_listen_page`, (ev) => { + if (!(ev instanceof CustomEvent)) return; + const { tag, value, from } = ev.detail; + // 仅打印 (TBC) + console.log(tag, value, from); + }); +}; + +// ================================ +// Server 构建与 service_worker 转发 +// ================================ + +type GmApiPayload = { api: string; params: any; uuid: string }; + +const handleRuntimeGmApi = ( + senderToInject: CustomEventMessage, + senderToContent: CustomEventMessage, + data: GmApiPayload +) => { + // 拦截关注的 API,未命中则返回 false 交由默认转发处理 + switch (data.api) { + case "CAT_createBlobUrl": { + const file = data.params[0] as File; + const url = makeBlobURL({ blob: file, persistence: false }) as string; + return url; + } + case "CAT_fetchBlob": { + return fetch(data.params[0]).then((res) => res.blob()); + } + case "CAT_fetchDocument": { + return new Promise((resolve) => { + const xhr = new XMLHttpRequest(); + xhr.responseType = "document"; + xhr.open("GET", data.params[0]); + xhr.onload = () => { + // TBC + const nodeId = (senderToInject).sendRelatedTarget(xhr.response); + resolve(nodeId); + }; + xhr.send(); + }); + } + case "GM_addElement": { + const [parentNodeId, tagName, tmpAttr, isContent] = data.params; + + // 根据来源选择不同的消息桥(content / inject) + const msg = isContent ? senderToContent : senderToInject; + + // 取回 parentNode(如果存在) + let parentNode: EventTarget | undefined; + if (parentNodeId) { + parentNode = msg.getAndDelRelatedTarget(parentNodeId); + } + + // 创建元素并设置属性 + const el = document.createElement(tagName); + const attr = tmpAttr ? { ...tmpAttr } : {}; + let textContent = ""; + if (attr.textContent) { + textContent = attr.textContent; + delete attr.textContent; + } + for (const key of Object.keys(attr)) { + el.setAttribute(key, attr[key]); + } + if (textContent) el.textContent = textContent; + + // 优先挂到 parentNode,否则挂到 head/body/任意节点 + const node = parentNode || document.head || document.body || document.querySelector("*"); + node.appendChild(el); + + // 返回节点引用 id,供另一侧再取回 + const nodeId = msg.sendRelatedTarget(el); + return nodeId; + } + case "GM_log": + // 拦截 GM_log:直接打印到控制台(某些页面可能劫持 console.log) + switch (data.params.length) { + case 1: + console.log(data.params[0]); + break; + case 2: + console.log("[" + data.params[1] + "]", data.params[0]); + break; + case 3: + console.log("[" + data.params[1] + "]", data.params[0], data.params[2]); + break; + } + break; + } + return false; +}; + +const prepareServer = ( + server: Server, + senderToExt: MessageSend, + senderToInject: CustomEventMessage, + senderToContent: CustomEventMessage +) => { + // service_worker 下发日志:统一打印 + server.on("logger", (data: Logger) => { + LoggerCore.logger().log(data.level, data.message, data.label); + }); + + // 将 inject/content 的请求转发到 service_worker + forwardMessage("serviceWorker", "script/isInstalled", server, senderToExt); + + // runtime/gmApi:对部分 API 做拦截处理 + forwardMessage("serviceWorker", "runtime/gmApi", server, senderToExt, (data: GmApiPayload) => { + return handleRuntimeGmApi(senderToInject, senderToContent, data); + }); }; -// ------------------------------ +// ================================ +// 握手:MessageFlag 与 injectFlagEvt 协商 +// ================================ + +/** + * 握手目标: + * - scripting 生成 injectFlagEvt(随机) + * - content/inject 通过 executorEnvReadyKey 收到 injectFlagEvt,并回发 emitterKey + * - 当 scripting 收到 inject+content 都 ready 后,才建立 server + delivery 通道 + */ +const onMessageFlagReceived = (MessageFlag: string) => { + const executorEnvReadyKey = uuidv5("scriptcat-executor-ready", MessageFlag); + + // 由 scripting 随机生成,用于 scripting <-> inject/content 的消息通道 token + const injectFlagEvt = randomMessageFlag(); + + // readyFlag 位运算:inject=1,content=2,凑齐 3 表示都 ready. ready 后设为 4 避免再触发 + let readyFlag = 0; + + const finalizeWhenReady = () => { + if (readyFlag === 3) { + readyFlag = 4; + + // 统一设置 token + scriptingMessaging.et = injectFlagEvt; + scriptExecutorMsgIT.et = `${injectFlagEvt}_${ScriptEnvTag.inject}`; + scriptExecutorMsgCT.et = `${injectFlagEvt}_${ScriptEnvTag.content}`; + + // 绑定 receiver(允许 inject/content 发消息给 scripting) + scriptExecutorMsgTxIT.bindReceiver(); + scriptExecutorMsgTxCT.bindReceiver(); + + // 建立 server:inject/content -> scripting 通道 + const server = new Server("scripting", [scriptExecutorMsgTxIT, scriptExecutorMsgTxCT]); + prepareServer(server, senderToExt, scriptExecutorMsgTxIT, scriptExecutorMsgTxCT); + + // 页面消息监听(TBC) + listenPageMessages(); + + // 建立向页面投递消息的 delivery 通道 + setupDeliveryChannel(); + } + }; + + // 接收 inject/content 的 ready 回执 + pageAddEventListener(`${injectFlagEvt}`, (ev) => { + if (!(ev instanceof CustomEvent)) return; + + const key = `emitterKeyFor${injectFlagEvt}`; + let value = ev.detail?.[key]; + if (!value) return; + + if (value !== ScriptEnvType.content) value = ScriptEnvType.inject; // 使 value 必定为 1 或 2 + readyFlag |= value; + finalizeWhenReady(); + }); + + // 向 inject/content 广播 injectFlagEvt(让它们知道后续用哪个 token 通信) + const submitTarget = () => { + return pageDispatchCustomEvent(executorEnvReadyKey, { injectFlagEvt }); + }; + + // 处理“scripting 早于 content/inject 执行”的场景: + // content/inject 会先发一个 executorEnvReadyKey(detail 为空)来探测 scripting 是否在 + pageAddEventListener(executorEnvReadyKey, (ev) => { + if (ev instanceof CustomEvent && !ev.detail) { + submitTarget(); + } + }); + + // 处理“scripting 晚于 content/inject 执行”的场景: + // scripting 启动后主动广播一次 executorEnvReadyKey,content/inject 立刻能收到 injectFlagEvt + submitTarget(); +}; + +// ================================ +// 来自 service_worker 的投递:storage 广播(类似 UDP) +// ================================ + +// 接数 service_worker 的 chrome.storage.local 值改变通知 (一对多广播) +// 类似 UDP 原理,service_worker 不会有任何「等待处理」 +// 由于 changes 会包括新旧值 (Chrome: JSON serialization, Firefox: Structured Clone) +// 因此需要注意资讯量不要过大导致 onChanged 的触发过慢 chrome.storage.local.onChanged.addListener((changes) => { if (changes["localStorage:scriptInjectMessageFlag"]?.newValue) { - dispatchDeliveryMessage({ + messageDeliveryToPage.dispatch({ tag: "localStorage:scriptInjectMessageFlag", value: changes["localStorage:scriptInjectMessageFlag"]?.newValue, }); } if (changes["valueUpdateDelivery"]?.newValue) { - dispatchDeliveryMessage({ + messageDeliveryToPage.dispatch({ tag: "valueUpdateDelivery", value: changes["valueUpdateDelivery"]?.newValue, }); } }); +// ================================ +// 来自 service_worker 的投递:runtime 一对一消息(类似 TCP) +// ================================ + +// 接收 service_worker 的 chrome.tabs.sendMessage (一对一消息) +// 类似 TCP 原理,service_worker 有「等待处理」 +// 由于 message 会包括值 (Chrome: JSON serialization, Firefox: Structured Clone) +// 因此需要注意资讯量不要过大导致 等待处理 时间过长 chrome.runtime.onMessage.addListener((message, _sender) => { if (!message) return; const { action, data } = message; - dispatchDeliveryMessage({ + messageDeliveryToPage.dispatch({ tag: action, value: data, }); }); -chrome.storage.local.get(["localStorage:scriptInjectMessageFlag"]).then((m) => { - const MessageFlag = m["localStorage:scriptInjectMessageFlag"].value; - - const mainKey = uuidv5("scriptcat-listen-inject", MessageFlag); - - const dispatchDeliveryMessageAfterEtSet = (detail: any) => { - if (!scriptingMessaging.et) throw new Error("scriptingMessaging is not ready or destroyed"); - pageDispatchCustomEvent(`evt_${scriptingMessaging.et}_deliveryMessage`, detail); - }; +// ================================ +// 启动流程 +// ================================ - const injectFlag = randomMessageFlag(); - const injectFlagEvt = injectFlag; - - // 用來接收 emitter - pageAddEventListener( - `${injectFlagEvt}`, - (ev) => { - if (ev instanceof CustomEvent && ev.detail?.[`emitterKeyFor${injectFlagEvt}`]) { - scriptingMessaging.et = ev.detail[`emitterKeyFor${injectFlagEvt}`]; - dispatchDeliveryMessage = dispatchDeliveryMessageAfterEtSet; - if (messageStack.length > 0) { - const messages = messageStack.slice(); - messageStack.length = 0; - for (const message of messages) { - dispatchDeliveryMessage(message); - } - } - } - }, - { once: true } - ); +// 1) scripting 直接读取 MessageFlag,并开始握手 +// scripting 直接调用 chrome.storage.local API 取得 MessageFlag +chrome.storage.local.get(["localStorage:scriptInjectMessageFlag"]).then((m) => { + const MessageFlag = m["localStorage:scriptInjectMessageFlag"].value; + onMessageFlagReceived(MessageFlag); +}); - const submitTarget = () => { - return pageDispatchCustomEvent(mainKey, { injectFlagEvt, scripting: true }); - }; +// 2) 向 service_worker 请求脚本列表及环境信息,并下发给 inject/content +// 向service_worker请求脚本列表及环境信息 +// - 以 ExtensionMessage 形式 从 scripting 发送到 service_worker 再以 Promise 形式取回 service_worker 结果 +client.pageLoad().then((o) => { + if (!o.ok) return; - if (submitTarget() === true) { - pageAddEventListener(mainKey, (ev) => { - if (ev instanceof CustomEvent && !ev.detail) { - submitTarget(); - } - }); + // 记录 uuid 来源:inject=1,content=2 + for (const entry of o.injectScriptList) { + uuids.set(entry.uuid, ScriptEnvType.inject); } + for (const entry of o.contentScriptList) { + uuids.set(entry.uuid, ScriptEnvType.content); + } + // 一次性广播给 inject 和 content + messageDeliveryToPage.dispatch({ + tag: "pageLoad", + value: o, + }); }); diff --git a/tests/vitest.setup.ts b/tests/vitest.setup.ts index 3cac3508f..a992f3f32 100644 --- a/tests/vitest.setup.ts +++ b/tests/vitest.setup.ts @@ -227,16 +227,3 @@ vi.stubGlobal("define", "特殊关键字不能穿透沙盒"); if (!URL.createObjectURL) URL.createObjectURL = undefined; //@ts-expect-error if (!URL.revokeObjectURL) URL.revokeObjectURL = undefined; - -// 测试环境使用 window 代替 performance 作为 EventTarget -performance.addEventListener = function (type: string, listener: any, options?: any) { - return window.addEventListener(type, listener, options); -}; - -performance.removeEventListener = function (type: string, listener: any, options?: any) { - return window.removeEventListener(type, listener, options); -}; - -performance.dispatchEvent = function (event: Event) { - return window.dispatchEvent(event); -}; From 2173bdd6ead00f11f6fcbb1508e56f96042e2eaa Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 1 Jan 2026 07:45:27 +0900 Subject: [PATCH 09/10] =?UTF-8?q?=E5=88=A0=E5=8E=BB=E4=B8=8D=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=20listenPageMessages,=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20CAT=5FfetchDocument?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/gm_api/gm_api.ts | 2 +- src/app/service/content/gm_api/gm_xhr.ts | 4 ++-- src/scripting.ts | 26 +++++------------------- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index f46d76976..9c1786c5a 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -481,7 +481,7 @@ export default class GMApi extends GM_Base { @GMContext.API() public async CAT_fetchDocument(url: string): Promise { - return urlToDocumentInContentPage(this, url); + return urlToDocumentInContentPage(this, url, isContent); } static _GM_cookie( diff --git a/src/app/service/content/gm_api/gm_xhr.ts b/src/app/service/content/gm_api/gm_xhr.ts index c7e5a1ca1..5f42a048e 100644 --- a/src/app/service/content/gm_api/gm_xhr.ts +++ b/src/app/service/content/gm_api/gm_xhr.ts @@ -94,9 +94,9 @@ export const convObjectToURL = async (object: string | URL | Blob | File | undef return url; }; -export const urlToDocumentInContentPage = async (a: GMApi, url: string) => { +export const urlToDocumentInContentPage = async (a: GMApi, url: string, isContent: boolean) => { // url (e.g. blob url) -> XMLHttpRequest (CONTENT) -> Document (CONTENT) - const nodeId = await a.sendMessage("CAT_fetchDocument", [url]); + const nodeId = await a.sendMessage("CAT_fetchDocument", [url, isContent]); return (a.message).getAndDelRelatedTarget(nodeId) as Document; }; diff --git a/src/scripting.ts b/src/scripting.ts index 255cd77c2..afc3d874c 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -65,21 +65,6 @@ const setupDeliveryChannel = () => { messageDeliveryToPage.setup(`evt_${token}_deliveryMessage`); }; -// ================================ -// 页面消息监听(调试用) -// ================================ - -// 页面消息监听(TBC) -const listenPageMessages = () => { - const token = requireScriptingToken(); - pageAddEventListener(`evt_${token}_listen_page`, (ev) => { - if (!(ev instanceof CustomEvent)) return; - const { tag, value, from } = ev.detail; - // 仅打印 (TBC) - console.log(tag, value, from); - }); -}; - // ================================ // Server 构建与 service_worker 转发 // ================================ @@ -102,13 +87,15 @@ const handleRuntimeGmApi = ( return fetch(data.params[0]).then((res) => res.blob()); } case "CAT_fetchDocument": { + const [url, isContent] = data.params; return new Promise((resolve) => { const xhr = new XMLHttpRequest(); xhr.responseType = "document"; - xhr.open("GET", data.params[0]); + xhr.open("GET", url); xhr.onload = () => { - // TBC - const nodeId = (senderToInject).sendRelatedTarget(xhr.response); + // 根据来源选择不同的消息桥(content / inject) + const msg = isContent ? senderToContent : senderToInject; + const nodeId = msg.sendRelatedTarget(xhr.response); resolve(nodeId); }; xhr.send(); @@ -221,9 +208,6 @@ const onMessageFlagReceived = (MessageFlag: string) => { const server = new Server("scripting", [scriptExecutorMsgTxIT, scriptExecutorMsgTxCT]); prepareServer(server, senderToExt, scriptExecutorMsgTxIT, scriptExecutorMsgTxCT); - // 页面消息监听(TBC) - listenPageMessages(); - // 建立向页面投递消息的 delivery 通道 setupDeliveryChannel(); } From 71b04a654fa54fc7b99276026eba093ebe1bb478 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 1 Jan 2026 09:14:00 +0900 Subject: [PATCH 10/10] =?UTF-8?q?=E9=87=8D=E6=9E=84=20MessageFlag=20?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=EF=BC=8C=E4=BF=AE=E6=AD=A3=20`pkg/utils/uuid?= =?UTF-8?q?`=20=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/custom_event_message.ts | 2 +- packages/message/window_message.ts | 2 +- rspack.config.ts | 4 +++- src/app/service/content/create_context.ts | 2 +- src/app/service/content/gm_api/gm_api.test.ts | 2 +- src/app/service/service_worker/client.ts | 2 +- src/app/service/service_worker/permission_verify.ts | 2 +- src/app/service/service_worker/runtime.ts | 8 +++----- src/app/service/service_worker/script.ts | 2 +- src/app/service/service_worker/subscribe.ts | 2 +- src/content.ts | 3 ++- src/inject.ts | 3 ++- src/pages/install/App.tsx | 2 +- src/pages/options/routes/script/ScriptEditor.tsx | 2 +- src/pkg/utils/match.test.ts | 2 +- src/pkg/utils/script.ts | 2 +- src/pkg/utils/uuid.ts | 2 +- src/scripting.ts | 9 ++++----- src/types/main.d.ts | 2 -- 19 files changed, 27 insertions(+), 28 deletions(-) diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index 55600df44..cfa8e7810 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -1,5 +1,5 @@ import type { Message, MessageConnect, RuntimeMessageSender, TMessage } from "./types"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import { type PostMessage, type WindowMessageBody, WindowMessageConnect } from "./window_message"; import EventEmitter from "eventemitter3"; import { DefinedFlags } from "@App/app/service/service_worker/runtime.consts"; diff --git a/packages/message/window_message.ts b/packages/message/window_message.ts index d650dedbf..baedbbe30 100644 --- a/packages/message/window_message.ts +++ b/packages/message/window_message.ts @@ -1,5 +1,5 @@ import type { Message, MessageConnect, MessageSend, RuntimeMessageSender, TMessage } from "./types"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import EventEmitter from "eventemitter3"; // 通过 window.postMessage/onmessage 实现通信 diff --git a/rspack.config.ts b/rspack.config.ts index 1c3b3a96d..a2c47f83a 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from "@rspack/cli"; import { rspack } from "@rspack/core"; import { readFileSync } from "fs"; import { NormalModule } from "@rspack/core"; +import { v4 as uuidv4 } from "uuid"; const pkg = JSON.parse(readFileSync("./package.json", "utf-8")); @@ -121,7 +122,8 @@ export default defineConfig({ }, plugins: [ new rspack.DefinePlugin({ - "process.env.VI_TESTING": "false", + "process.env.VI_TESTING": "'false'", + "process.env.SC_RANDOM_KEY": `'${uuidv4()}'`, }), new rspack.CopyRspackPlugin({ patterns: [ diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 69924277a..ce4f4994c 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -1,5 +1,5 @@ import type { TScriptInfo } from "@App/app/repo/scripts"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import type { Message } from "@Packages/message/types"; import EventEmitter from "eventemitter3"; import { GMContextApiGet } from "./gm_api/gm_context"; diff --git a/src/app/service/content/gm_api/gm_api.test.ts b/src/app/service/content/gm_api/gm_api.test.ts index 9a97c1b3e..5bcb55227 100644 --- a/src/app/service/content/gm_api/gm_api.test.ts +++ b/src/app/service/content/gm_api/gm_api.test.ts @@ -5,7 +5,7 @@ import type { GMInfoEnv, ScriptFunc } from "../types"; import { compileScript, compileScriptCode } from "../utils"; import type { Message } from "@Packages/message/types"; import { encodeRValue } from "@App/pkg/utils/message_value"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; const nilFn: ScriptFunc = () => {}; const scriptRes = { diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index da64bf805..f71ae2f12 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -8,7 +8,7 @@ import type { MessageSend } from "@Packages/message/types"; import type PermissionVerify from "./permission_verify"; import { type UserConfirm } from "./permission_verify"; import { type FileSystemType } from "@Packages/filesystem/factory"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import { cacheInstance } from "@App/app/cache"; import { CACHE_KEY_IMPORT_FILE } from "@App/app/cache_key"; import { type ResourceBackup } from "@App/pkg/backup/struct"; diff --git a/src/app/service/service_worker/permission_verify.ts b/src/app/service/service_worker/permission_verify.ts index 2beefcaaf..d241287b9 100644 --- a/src/app/service/service_worker/permission_verify.ts +++ b/src/app/service/service_worker/permission_verify.ts @@ -7,7 +7,7 @@ import type { IMessageQueue } from "@Packages/message/message_queue"; import type { Api, GMApiRequest } from "./types"; import { cacheInstance } from "@App/app/cache"; import { CACHE_KEY_PERMISSION } from "@App/app/cache_key"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import Queue from "@App/pkg/utils/queue"; import { type TDeleteScript } from "../queue"; import { openInCurrentTab } from "@App/pkg/utils/utils"; diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index bab1685d7..d7ad09965 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -825,7 +825,6 @@ export class RuntimeService { excludeMatches: string[]; excludeGlobs: string[]; }) { - const messageFlag = runtimeGlobal.messageFlag; // 配置脚本运行环境: 注册时前先准备 chrome.runtime 等设定 // Firefox MV3 只提供 runtime.sendMessage 及 runtime.connect // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts/WorldProperties#messaging @@ -851,7 +850,7 @@ export class RuntimeService { const injectJs = await this.getInjectJsCode(); if (injectJs) { // 构建inject.js的脚本注册信息 - retInject = this.compileInjectUserScript(injectJs, messageFlag, { + retInject = this.compileInjectUserScript(injectJs, { excludeMatches, excludeGlobs, }); @@ -871,7 +870,7 @@ export class RuntimeService { const contentJs = await this.getContentJsCode(); if (contentJs) { - const codeBody = `(function (MessageFlag) {\n${contentJs}\n})('${messageFlag}')`; + const codeBody = `(function () {\n${contentJs}\n})()`; const code = `${codeBody}${sourceMapTo("scriptcat-content.js")}\n`; retInject.push({ id: "scriptcat-content", @@ -1333,11 +1332,10 @@ export class RuntimeService { compileInjectUserScript( injectJs: string, - messageFlag: string, { excludeMatches, excludeGlobs }: { excludeMatches: string[] | undefined; excludeGlobs: string[] | undefined } ) { // 构建inject.js的脚本注册信息 - const codeBody = `(function (MessageFlag,UserAgentData) {\n${injectJs}\n})('${messageFlag}', ${JSON.stringify(this.userAgentData)})`; + const codeBody = `(function (UserAgentData) {\n${injectJs}\n})(${JSON.stringify(this.userAgentData)})`; const code = `${codeBody}${sourceMapTo("scriptcat-inject.js")}\n`; const script: chrome.userScripts.RegisteredUserScript = { id: "scriptcat-inject", diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 4c9fdc3bb..29b5868e5 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -1,5 +1,5 @@ import { fetchScriptBody, parseMetadata, prepareScriptByCode } from "@App/pkg/utils/script"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import type { Group } from "@Packages/message/server"; import Logger from "@App/app/logger/logger"; import LoggerCore from "@App/app/logger/core"; diff --git a/src/app/service/service_worker/subscribe.ts b/src/app/service/service_worker/subscribe.ts index 9f7708f8f..479e9b0c1 100644 --- a/src/app/service/service_worker/subscribe.ts +++ b/src/app/service/service_worker/subscribe.ts @@ -13,7 +13,7 @@ import { checkSilenceUpdate, InfoNotification } from "@App/pkg/utils/utils"; import { ltever } from "@App/pkg/utils/semver"; import { fetchScriptBody, parseMetadata, prepareSubscribeByCode } from "@App/pkg/utils/script"; import { cacheInstance } from "@App/app/cache"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key"; import i18n, { i18nName } from "@App/locales/locales"; diff --git a/src/content.ts b/src/content.ts index ae1d7afc0..2b05f25fd 100644 --- a/src/content.ts +++ b/src/content.ts @@ -8,7 +8,8 @@ import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_execut import type { ValueUpdateDataEncoded } from "./app/service/content/types"; import type { TClientPageLoadInfo } from "./app/repo/scripts"; -/* global MessageFlag */ +//@ts-ignore +const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY); // ================================ // 常量与全局状态 diff --git a/src/inject.ts b/src/inject.ts index a26154690..7d92e860d 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -11,7 +11,8 @@ import type { Message } from "@Packages/message/types"; import { sendMessage } from "@Packages/message/client"; import { ExternalWhitelist } from "@App/app/const"; -/* global MessageFlag */ +//@ts-ignore +const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY); // ================================ // 常量与全局状态 diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 5770bf2b8..6807cd78d 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -12,7 +12,7 @@ import { Popover, } from "@arco-design/web-react"; import { IconDown } from "@arco-design/web-react/icon"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import CodeEditor from "../components/CodeEditor"; import { useEffect, useMemo, useState } from "react"; import type { SCMetadata, Script } from "@App/app/repo/scripts"; diff --git a/src/pages/options/routes/script/ScriptEditor.tsx b/src/pages/options/routes/script/ScriptEditor.tsx index 4ba5a5f0e..044cf6a09 100644 --- a/src/pages/options/routes/script/ScriptEditor.tsx +++ b/src/pages/options/routes/script/ScriptEditor.tsx @@ -10,7 +10,7 @@ import TabPane from "@arco-design/web-react/es/Tabs/tab-pane"; import normalTpl from "@App/template/normal.tpl"; import crontabTpl from "@App/template/crontab.tpl"; import backgroundTpl from "@App/template/background.tpl"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import "./index.css"; import LoggerCore from "@App/app/logger/core"; import Logger from "@App/app/logger/logger"; diff --git a/src/pkg/utils/match.test.ts b/src/pkg/utils/match.test.ts index a9c5497e7..eeec35dac 100644 --- a/src/pkg/utils/match.test.ts +++ b/src/pkg/utils/match.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { isUrlExcluded, isUrlIncluded, UrlMatch } from "./match"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import { extractUrlPatterns } from "./url_matcher"; describe.concurrent("UrlMatch-internal1", () => { diff --git a/src/pkg/utils/script.ts b/src/pkg/utils/script.ts index b498976e0..35da8a242 100644 --- a/src/pkg/utils/script.ts +++ b/src/pkg/utils/script.ts @@ -1,4 +1,4 @@ -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import type { SCMetadata, Script, ScriptCode, UserConfig } from "@App/app/repo/scripts"; import { SCRIPT_RUN_STATUS_COMPLETE, diff --git a/src/pkg/utils/uuid.ts b/src/pkg/utils/uuid.ts index d86b32696..73175e511 100644 --- a/src/pkg/utils/uuid.ts +++ b/src/pkg/utils/uuid.ts @@ -1,3 +1,3 @@ import { v4, v5 } from "uuid"; -export const uuidv4 = typeof crypto.randomUUID === "function" ? () => crypto.randomUUID() : v4; +export const uuidv4 = typeof crypto.randomUUID === "function" ? crypto.randomUUID.bind(crypto) : v4; export const uuidv5 = v5; diff --git a/src/scripting.ts b/src/scripting.ts index afc3d874c..9976eb10e 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -12,6 +12,9 @@ import { RuntimeClient } from "@App/app/service/service_worker/client"; import type { Logger } from "@App/app/repo/logger"; import { MessageDelivery } from "./message-delivery"; +//@ts-ignore +const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY); + // ================================ // 常量与全局状态 // ================================ @@ -289,11 +292,7 @@ chrome.runtime.onMessage.addListener((message, _sender) => { // ================================ // 1) scripting 直接读取 MessageFlag,并开始握手 -// scripting 直接调用 chrome.storage.local API 取得 MessageFlag -chrome.storage.local.get(["localStorage:scriptInjectMessageFlag"]).then((m) => { - const MessageFlag = m["localStorage:scriptInjectMessageFlag"].value; - onMessageFlagReceived(MessageFlag); -}); +onMessageFlagReceived(MessageFlag); // 2) 向 service_worker 请求脚本列表及环境信息,并下发给 inject/content // 向service_worker请求脚本列表及环境信息 diff --git a/src/types/main.d.ts b/src/types/main.d.ts index ace032a79..f04a13904 100644 --- a/src/types/main.d.ts +++ b/src/types/main.d.ts @@ -28,8 +28,6 @@ interface FileSystemObserverInstance { observe(handle: FileSystemFileHandle | FileSystemDirectoryHandle | FileSystemSyncAccessHandle): Promise; } -declare const MessageFlag: string; - declare const UserAgentData: typeof GM_info.userAgentData; // 可以让content与inject环境交换携带dom的对象