diff --git a/packages/message/common.ts b/packages/message/common.ts new file mode 100644 index 000000000..222eed3d7 --- /dev/null +++ b/packages/message/common.ts @@ -0,0 +1,18 @@ +// 避免页面载入后改动全域物件导致消息传递失败 +export const MouseEventClone = MouseEvent; +export const CustomEventClone = CustomEvent; +const performanceClone = performance; + +// 避免页面载入后改动 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, performanceClone); + 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 b3e8cf952..933681b29 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -1,17 +1,16 @@ 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); - -// 避免页面载入后改动全域物件导致消息传递失败 -const MouseEventClone = MouseEvent; -const CustomEventClone = CustomEvent; +import { + pageDispatchEvent, + pageAddEventListener, + pageRemoveEventListener, + pageDispatchCustomEvent, + MouseEventClone, + CustomEventClone, +} from "@Packages/message/common"; // 避免页面载入后改动 Map.prototype 导致消息传递失败 const relatedTargetMap = new Map(); @@ -30,28 +29,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 +125,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 +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."); const messageId = uuidv4(); const body: WindowMessageBody = { messageId, @@ -164,11 +187,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/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/app/service/content/utils.ts b/src/app/service/content/utils.ts index 0c2c1c5c4..4e9cf3f9e 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, performance) : 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 2c9a60a9e..4efd2057b 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" }); @@ -341,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; @@ -668,13 +671,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() { @@ -848,34 +853,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..d28b314cd 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,51 +1,118 @@ 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 } 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"; -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 = () => {}; +// ------------ 對象 ------------ + +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)); +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))); + } + 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); -} +// ------------ 請求 ------------ +pageDispatchEvent(new CustomEvent(mainKey)); +// ----------------------------- diff --git a/src/inject.ts b/src/inject.ts index b434858d1..8eed4c49e 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -1,16 +1,22 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; -import { CustomEventMessage } 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"; 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 +39,30 @@ server.on("pageLoad", (data: { injectScriptList: TScriptInfo[]; envInfo: GMInfoE runtime.startScripts(data.injectScriptList, data.envInfo); runtime.onInjectPageLoaded(); }); + +const injectFlag = randomMessageFlag(); +const injectFlagEvt = injectFlag; + +// 用來接收 emitter +pageAddEventListener( + `${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) { + pageAddEventListener(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..223bc5258 --- /dev/null +++ b/src/scripting.ts @@ -0,0 +1,82 @@ +import { randomMessageFlag } from "./pkg/utils/utils"; +import { createPageMessaging } from "@Packages/message/custom_event_message"; +import { pageAddEventListener, pageDispatchCustomEvent } from "@Packages/message/common"; +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 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 } + ); + + const submitTarget = () => { + return pageDispatchCustomEvent(mainKey, { injectFlagEvt, scripting: true }); + }; + + if (submitTarget() === true) { + pageAddEventListener(mainKey, (ev) => { + if (ev instanceof CustomEvent && !ev.detail) { + submitTarget(); + } + }); + } +});