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/33] =?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/33] =?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/33] =?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/33] =?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/33] =?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/33] =?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/33] =?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/33] =?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/33] =?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/33] =?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的对象 From f0d3ec2415929b1a0668d37295d7d2e19c27f998 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:25:40 +0900 Subject: [PATCH 11/33] Update src/app/service/content/exec_script.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/service/content/exec_script.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/content/exec_script.ts b/src/app/service/content/exec_script.ts index dcdb99259..9ec3c701d 100644 --- a/src/app/service/content/exec_script.ts +++ b/src/app/service/content/exec_script.ts @@ -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上下文 From 8f92865fc7628fb9785d5276c892b2010ead1647 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:31:59 +0900 Subject: [PATCH 12/33] =?UTF-8?q?=E6=B3=A8=E9=87=8A=E4=B8=AD"=E6=8E=A5?= =?UTF-8?q?=E6=95=B0"=E5=BA=94=E4=B8=BA"=E6=8E=A5=E6=94=B6"=E3=80=82?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E8=A1=A8=E8=BF=B0=E5=BA=94=E4=B8=BA"?= =?UTF-8?q?=E6=8E=A5=E6=94=B6=20service=5Fworker=20=E7=9A=84=20chrome.stor?= =?UTF-8?q?age.local=20=E5=80=BC=E6=94=B9=E5=8F=98=E9=80=9A=E7=9F=A5"?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripting.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripting.ts b/src/scripting.ts index 363592d82..e991c4ba6 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -251,7 +251,7 @@ const onMessageFlagReceived = (MessageFlag: string) => { // 来自 service_worker 的投递:storage 广播(类似 UDP) // ================================ -// 接数 service_worker 的 chrome.storage.local 值改变通知 (一对多广播) +// 接收 service_worker 的 chrome.storage.local 值改变通知 (一对多广播) // 类似 UDP 原理,service_worker 不会有任何「等待处理」 // 由于 changes 会包括新旧值 (Chrome: JSON serialization, Firefox: Structured Clone) // 因此需要注意资讯量不要过大导致 onChanged 的触发过慢 From bf09956bbe942dc4d3d2eebe040e259263085662 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:46:30 +0900 Subject: [PATCH 13/33] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20createPageMessaging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/custom_event_message.ts | 23 ++++++++++++++++++----- src/content.ts | 8 ++++---- src/inject.ts | 8 ++++---- src/scripting.ts | 6 +++--- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index cfa8e7810..35a72e5de 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -32,23 +32,36 @@ export class CustomEventPostMessage implements PostMessage { export type PageMessaging = { et: string; bindReceiver?: () => void; - waitReady?: Promise; waitReadyResolve?: () => any; onReady?: (callback: () => any) => any; + setMessageTag: (tag: string) => void; + clearMessageTag: () => void; }; export const createPageMessaging = (et: string) => { const pageMessaging = { et } as PageMessaging; - pageMessaging.waitReady = new Promise((resolve) => { - pageMessaging.waitReadyResolve = resolve; - }); + let resolveFn: ((value: void | PromiseLike) => void) | null = null; + let promise = et + ? null + : new Promise((resolve) => { + resolveFn = resolve; + }); pageMessaging.onReady = (callback: () => any) => { if (pageMessaging.et) { callback(); } else { - pageMessaging.waitReady!.then(callback); + promise?.then(callback); } }; + pageMessaging.setMessageTag = function (tag: string) { + if (this.et) throw new Error("pageMessaging.et has already been set."); + this.et = tag; + resolveFn?.(); + promise = null; + }; + pageMessaging.clearMessageTag = function () { + this.et = ""; + }; return pageMessaging; }; diff --git a/src/content.ts b/src/content.ts index 2b05f25fd..f842c8342 100644 --- a/src/content.ts +++ b/src/content.ts @@ -57,14 +57,14 @@ const requireScriptingToken = (): string => { // 重置所有页面通信 token(用于反注册脚本) const resetMessagingTokens = () => { - scriptingMessaging.et = ""; - pageMessaging.et = ""; + scriptingMessaging.clearMessageTag(); + pageMessaging.clearMessageTag(); }; // 根据 injectFlagEvt 设置双方通信 token const setMessagingTokens = (injectFlagEvt: string) => { - scriptingMessaging.et = injectFlagEvt; - pageMessaging.et = `${injectFlagEvt}_${scriptEnvTag}`; + scriptingMessaging.setMessageTag(injectFlagEvt); + pageMessaging.setMessageTag(`${injectFlagEvt}_${scriptEnvTag}`); }; // 通知 scripting 侧:content 已完成初始化 diff --git a/src/inject.ts b/src/inject.ts index 7d92e860d..2c603179b 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -60,14 +60,14 @@ const requireScriptingToken = (): string => { // 重置所有页面通信 token(用于反注册脚本) const resetMessagingTokens = () => { - scriptingMessaging.et = ""; - pageMessaging.et = ""; + scriptingMessaging.clearMessageTag(); + pageMessaging.clearMessageTag(); }; // 根据 injectFlagEvt 设置双方通信 token const setMessagingTokens = (injectFlagEvt: string) => { - scriptingMessaging.et = injectFlagEvt; - pageMessaging.et = `${injectFlagEvt}_${scriptEnvTag}`; + scriptingMessaging.setMessageTag(injectFlagEvt); + pageMessaging.setMessageTag(`${injectFlagEvt}_${scriptEnvTag}`); }; // 通知 scripting 侧:inject 已完成初始化 diff --git a/src/scripting.ts b/src/scripting.ts index e991c4ba6..7ff48cf96 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -199,9 +199,9 @@ const onMessageFlagReceived = (MessageFlag: string) => { readyFlag = 4; // 统一设置 token - scriptingMessaging.et = injectFlagEvt; - scriptExecutorMsgIT.et = `${injectFlagEvt}_${ScriptEnvTag.inject}`; - scriptExecutorMsgCT.et = `${injectFlagEvt}_${ScriptEnvTag.content}`; + scriptingMessaging.setMessageTag(injectFlagEvt); + scriptExecutorMsgIT.setMessageTag(`${injectFlagEvt}_${ScriptEnvTag.inject}`); + scriptExecutorMsgCT.setMessageTag(`${injectFlagEvt}_${ScriptEnvTag.content}`); // 绑定 receiver(允许 inject/content 发消息给 scripting) scriptExecutorMsgTxIT.bindReceiver(); From 0ef870a0b9789b28962aa6280d6d020c4fce2338 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:47:00 +0900 Subject: [PATCH 14/33] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20createPageMessaging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/custom_event_message.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index 35a72e5de..3338a1716 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -32,7 +32,6 @@ export class CustomEventPostMessage implements PostMessage { export type PageMessaging = { et: string; bindReceiver?: () => void; - waitReadyResolve?: () => any; onReady?: (callback: () => any) => any; setMessageTag: (tag: string) => void; clearMessageTag: () => void; From b48865cae8bbb3b0f650b764a0d3f7a55d1b38a6 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:50:38 +0900 Subject: [PATCH 15/33] Update src/app/service/service_worker/value.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/service/service_worker/value.ts | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index be3403367..48cf5143a 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -77,36 +77,12 @@ export class ValueService { // 推送值到tab async pushValueToTab(sendData: T) { - // 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( { From 444930985af8ff10881d47766577b111bbfdb868 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:50:51 +0900 Subject: [PATCH 16/33] Update src/app/service/service_worker/runtime.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/service/service_worker/runtime.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 8ceee20f5..8e3f8f241 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -691,7 +691,6 @@ export class RuntimeService { // 生成messageFlag generateMessageFlag(): string { - // return randomMessageFlag(); return uuidv4(); } From 06b52ae51b8498b460fea86c572c1e89efba389e Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:25:32 +0900 Subject: [PATCH 17/33] =?UTF-8?q?=E6=B3=A8=E9=87=8A=E8=A1=A5=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/content.ts | 4 ++-- src/inject.ts | 4 ++-- src/scripting.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/content.ts b/src/content.ts index f842c8342..2d09a844a 100644 --- a/src/content.ts +++ b/src/content.ts @@ -61,7 +61,7 @@ const resetMessagingTokens = () => { pageMessaging.clearMessageTag(); }; -// 根据 injectFlagEvt 设置双方通信 token +// 根据 injectFlagEvt 设置双方通信 token(仅允许调用一次) const setMessagingTokens = (injectFlagEvt: string) => { scriptingMessaging.setMessageTag(injectFlagEvt); pageMessaging.setMessageTag(`${injectFlagEvt}_${scriptEnvTag}`); @@ -141,7 +141,7 @@ const setupHandshake = () => { bindScriptingDeliveryChannel(); }; - // 等待 scripting 注入完成并发送 injectFlagEvt + // 等待 scripting 注入完成并发送 injectFlagEvt (仅调用一次) pageAddEventListener(executorEnvReadyKey, (ev) => { if (!(ev instanceof CustomEvent)) return; diff --git a/src/inject.ts b/src/inject.ts index 2c603179b..fc8e04252 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -64,7 +64,7 @@ const resetMessagingTokens = () => { pageMessaging.clearMessageTag(); }; -// 根据 injectFlagEvt 设置双方通信 token +// 根据 injectFlagEvt 设置双方通信 token(仅允许调用一次) const setMessagingTokens = (injectFlagEvt: string) => { scriptingMessaging.setMessageTag(injectFlagEvt); pageMessaging.setMessageTag(`${injectFlagEvt}_${scriptEnvTag}`); @@ -230,7 +230,7 @@ const setupHandshake = () => { bindScriptingDeliveryChannel(); }; - // 等待 scripting 注入完成并发送 injectFlagEvt + // 等待 scripting 注入完成并发送 injectFlagEvt (仅调用一次) pageAddEventListener(executorEnvReadyKey, (ev) => { if (!(ev instanceof CustomEvent)) return; diff --git a/src/scripting.ts b/src/scripting.ts index 7ff48cf96..7ebb2d995 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -196,7 +196,7 @@ const onMessageFlagReceived = (MessageFlag: string) => { const finalizeWhenReady = () => { if (readyFlag === 3) { - readyFlag = 4; + readyFlag = 4; // 确保单次调用限制 // 统一设置 token scriptingMessaging.setMessageTag(injectFlagEvt); From 10941fe6364d3a7dbcea81a0845b3852099cdd2c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:12:41 +0900 Subject: [PATCH 18/33] Update scripting.ts --- src/scripting.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripting.ts b/src/scripting.ts index 7ebb2d995..1794344e9 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -237,7 +237,7 @@ const onMessageFlagReceived = (MessageFlag: string) => { // 处理“scripting 早于 content/inject 执行”的场景: // content/inject 会先发一个 executorEnvReadyKey(detail 为空)来探测 scripting 是否在 pageAddEventListener(executorEnvReadyKey, (ev) => { - if (ev instanceof CustomEvent && !ev.detail) { + if (readyFlag < 3 && ev instanceof CustomEvent && !ev.detail) { submitTarget(); } }); From d37dee2c1fabaeb755d3bdc74eee5a8fd9d0ab38 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 24 Jan 2026 09:25:46 +0900 Subject: [PATCH 19/33] fix early-start --- src/app/service/content/script_executor.ts | 6 +++--- src/app/service/content/utils.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index 181bbb062..a9d1b17e3 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, type ScriptEnvTag } from "@Packages/message/common"; +import { pageAddEventListener, pageDispatchEvent, type ScriptEnvTag } from "@Packages/message/common"; import { isUrlExcluded } from "@App/pkg/utils/match"; export type ExecScriptEntry = { @@ -86,12 +86,12 @@ export class ScriptExecutor { } checkEarlyStartScript(scriptEnvTag: ScriptEnvTag, messageFlag: string, envInfo: GMInfoEnv) { - const eventNamePrefix = `evt${messageFlag}.${scriptEnvTag}`; + const eventNamePrefix = `evt${process.env.SC_RANDOM_KEY}.${scriptEnvTag}`; // 仅用于early-start初始化 const scriptLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.scriptLoadComplete}`; const envLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.envLoadComplete}`; // 监听 脚本加载 // 适用于此「通知环境加载完成」代码执行后的脚本加载 - performance.addEventListener(scriptLoadCompleteEvtName, (ev) => { + pageAddEventListener(scriptLoadCompleteEvtName, (ev) => { const detail = (ev as CustomEvent).detail as { scriptFlag: string; scriptInfo: ScriptLoadInfo; diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index 647fc028a..45fc01fc7 100644 --- a/src/app/service/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -141,7 +141,7 @@ export function compilePreInjectScript( autoDeleteMountFunction: boolean = false ): string { const scriptEnvTag = isInjectIntoContent(script.metadata) ? ScriptEnvTag.content : ScriptEnvTag.inject; - const eventNamePrefix = `evt${messageFlag}.${scriptEnvTag}`; + const eventNamePrefix = `evt${process.env.SC_RANDOM_KEY}.${scriptEnvTag}`; // 仅用于early-start初始化 const flag = `${script.flag}`; const scriptInfo = trimScriptInfo(script); const scriptInfoJSON = `${JSON.stringify(scriptInfo)}`; From fb4b8d0d0a20510d464a8277b19e822571dd08d1 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 24 Jan 2026 09:35:33 +0900 Subject: [PATCH 20/33] =?UTF-8?q?=E5=88=A0=E9=99=A4early-start=E7=94=A8?= =?UTF-8?q?=E7=9A=84=20messageFlag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/script_executor.ts | 2 +- src/app/service/content/utils.ts | 1 - src/app/service/service_worker/runtime.ts | 16 +++------------- src/app/service/service_worker/utils.ts | 7 +------ src/content.ts | 2 +- src/inject.ts | 2 +- 6 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index a9d1b17e3..c2796039b 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -85,7 +85,7 @@ export class ScriptExecutor { }); } - checkEarlyStartScript(scriptEnvTag: ScriptEnvTag, messageFlag: string, envInfo: GMInfoEnv) { + checkEarlyStartScript(scriptEnvTag: ScriptEnvTag, envInfo: GMInfoEnv) { const eventNamePrefix = `evt${process.env.SC_RANDOM_KEY}.${scriptEnvTag}`; // 仅用于early-start初始化 const scriptLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.scriptLoadComplete}`; const envLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.envLoadComplete}`; diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index 45fc01fc7..83b458f02 100644 --- a/src/app/service/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -135,7 +135,6 @@ export const trimScriptInfo = (script: ScriptLoadInfo): TScriptInfo => { * 将脚本函数编译为预注入脚本代码 */ export function compilePreInjectScript( - messageFlag: string, script: ScriptLoadInfo, scriptCode: string, autoDeleteMountFunction: boolean = false diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 8e3f8f241..b52bf484d 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -710,12 +710,7 @@ export class RuntimeService { let jsCode = ""; if (withCode) { - const code = compileInjectionCode( - this.getMessageFlag(), - scriptRes, - scriptRes.code, - scriptMatchInfo.scriptUrlPatterns - ); + const code = compileInjectionCode(scriptRes, scriptRes.code, scriptMatchInfo.scriptUrlPatterns); registerScript.js[0].code = jsCode = code; } @@ -758,7 +753,7 @@ export class RuntimeService { if (earlyScript) { const scriptRes = await this.script.buildScriptRunResource(script); if (!scriptRes) return ""; - return compileInjectionCode(this.getMessageFlag(), scriptRes, scriptRes.code, result.scriptUrlPatterns); + return compileInjectionCode(scriptRes, scriptRes.code, result.scriptUrlPatterns); } const originalCode = await this.script.scriptCodeDAO.get(result.uuid); @@ -1264,12 +1259,7 @@ export class RuntimeService { const scriptRes = scriptsWithUpdatedResources.get(targetUUID); const scriptDAOCode = scriptCodes[targetUUID]; if (scriptRes && scriptDAOCode) { - const scriptInjectCode = compileInjectionCode( - this.getMessageFlag(), - scriptRes, - scriptDAOCode, - scriptRes.scriptUrlPatterns! - ); + const scriptInjectCode = compileInjectionCode(scriptRes, scriptDAOCode, scriptRes.scriptUrlPatterns!); scriptRegisterInfo.js = [ { code: scriptInjectCode, diff --git a/src/app/service/service_worker/utils.ts b/src/app/service/service_worker/utils.ts index 493642c17..9e7adfeba 100644 --- a/src/app/service/service_worker/utils.ts +++ b/src/app/service/service_worker/utils.ts @@ -169,7 +169,6 @@ export function parseScriptLoadInfo(script: ScriptRunResource, scriptUrlPatterns } export function compileInjectionCode( - messageFlag: string, scriptRes: ScriptRunResource, scriptCode: string, scriptUrlPatterns: URLRuleEntry[] @@ -178,11 +177,7 @@ export function compileInjectionCode( let scriptInjectCode; scriptCode = compileScriptCode(scriptRes, scriptCode); if (preDocumentStartScript) { - scriptInjectCode = compilePreInjectScript( - messageFlag, - parseScriptLoadInfo(scriptRes, scriptUrlPatterns), - scriptCode - ); + scriptInjectCode = compilePreInjectScript(parseScriptLoadInfo(scriptRes, scriptUrlPatterns), scriptCode); } else { scriptInjectCode = compileInjectScript(scriptRes, scriptCode); } diff --git a/src/content.ts b/src/content.ts index 2d09a844a..e0a94e43c 100644 --- a/src/content.ts +++ b/src/content.ts @@ -174,7 +174,7 @@ const setupHandshake = () => { // ================================ // 检查 early-start 脚本 -scriptExecutor.checkEarlyStartScript(scriptEnvTag, MessageFlag, initEnvInfo); +scriptExecutor.checkEarlyStartScript(scriptEnvTag, initEnvInfo); // 建立握手与通信绑定 setupHandshake(); diff --git a/src/inject.ts b/src/inject.ts index fc8e04252..7e25fef8e 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -263,7 +263,7 @@ const setupHandshake = () => { // ================================ // 检查 early-start 脚本 -scriptExecutor.checkEarlyStartScript(scriptEnvTag, MessageFlag, initEnvInfo); +scriptExecutor.checkEarlyStartScript(scriptEnvTag, initEnvInfo); // 建立握手与通信绑定 setupHandshake(); From ff2538afee6e98dd90768b3c8d38c60f4e438cbf Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:12:00 +0900 Subject: [PATCH 21/33] vitest env fix --- packages/message/common.ts | 3 +- packages/message/server.test.ts | 80 ++++++++++++++++----------------- tests/vitest.setup.ts | 6 +++ 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index 51f51b95b..2c05f0e67 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -15,7 +15,8 @@ export type ScriptEnvType = ValueOf; // 避免页面载入后改动全域物件导致消息传递失败 export const MouseEventClone = MouseEvent; export const CustomEventClone = CustomEvent; -const performanceClone = process.env.VI_TESTING === "true" ? window : performance; +//@ts-ignore +const performanceClone = process.env.VI_TESTING === "true" ? simulatedEventTarget : performance; // 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败 export const pageDispatchEvent = performanceClone.dispatchEvent.bind(performanceClone); diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index 603cb6f4e..b4f0a4a1a 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -27,50 +27,46 @@ const setupGlobal = () => { // 客户端使用 inject / content 消息 client = outboundMessage; - // 清理 DOM 事件监听器 - vi.stubGlobal("window", Object.create(window)); - vi.stubGlobal("addEventListener", vi.fn()); + //@ts-ignore + const window = simulatedEventTarget; // 模拟消息传递 - 从 inject 到 content - vi.stubGlobal( - "dispatchEvent", - vi.fn().mockImplementation((event: Event) => { - if (event instanceof CustomEvent) { - const eventType = event.type; - if (eventType.includes(testFlag)) { - let targetEventType: string; - let messageThis: CustomEventMessage; - let messageThat: CustomEventMessage; - // 根据事件类型确定目标消息处理器 - if (eventType.includes(DefinedFlags.inboundFlag)) { - // inject -> content - targetEventType = eventType.replace(DefinedFlags.inboundFlag, DefinedFlags.outboundFlag); - messageThis = inboundMessage; - messageThat = outboundMessage; - } else if (eventType.includes(DefinedFlags.outboundFlag)) { - // content -> inject - targetEventType = eventType.replace(DefinedFlags.outboundFlag, DefinedFlags.inboundFlag); - messageThis = outboundMessage; - messageThat = inboundMessage; - } else { - throw new Error("test mock failed"); - } - nextTick().then(() => { - messageThis.messageHandle(event.detail, { - postMessage: (data: any) => { - // 响应 - const responseEvent = new CustomEvent(targetEventType, { detail: data }); - messageThat.messageHandle(responseEvent.detail, { - postMessage: vi.fn(), - }); - }, - }); - }); + window.dispatchEvent.mockImplementation((event: Event) => { + if (event instanceof CustomEvent) { + const eventType = event.type; + if (eventType.includes(testFlag)) { + let targetEventType: string; + let messageThis: CustomEventMessage; + let messageThat: CustomEventMessage; + // 根据事件类型确定目标消息处理器 + if (eventType.includes(DefinedFlags.inboundFlag)) { + // inject -> content + targetEventType = eventType.replace(DefinedFlags.inboundFlag, DefinedFlags.outboundFlag); + messageThis = inboundMessage; + messageThat = outboundMessage; + } else if (eventType.includes(DefinedFlags.outboundFlag)) { + // content -> inject + targetEventType = eventType.replace(DefinedFlags.outboundFlag, DefinedFlags.inboundFlag); + messageThis = outboundMessage; + messageThat = inboundMessage; + } else { + throw new Error("test mock failed"); } + nextTick().then(() => { + messageThis.messageHandle(event.detail, { + postMessage: (data: any) => { + // 响应 + const responseEvent = new CustomEvent(targetEventType, { detail: data }); + messageThat.messageHandle(responseEvent.detail, { + postMessage: vi.fn(), + }); + }, + }); + }); } - return true; - }) - ); + } + return true; + }); }; beforeEach(() => { @@ -665,10 +661,10 @@ describe("Server", () => { it.concurrent("应该能够处理空参数", async () => { const mockHandler = vi.fn().mockResolvedValue("empty response"); - server.on("on-empty", mockHandler); + server.on("on-test-empty", mockHandler); const response = await client.sendMessage({ - action: "api/on-empty", + action: "api/on-test-empty", data: null, }); diff --git a/tests/vitest.setup.ts b/tests/vitest.setup.ts index a992f3f32..a4972e4f7 100644 --- a/tests/vitest.setup.ts +++ b/tests/vitest.setup.ts @@ -227,3 +227,9 @@ vi.stubGlobal("define", "特殊关键字不能穿透沙盒"); if (!URL.createObjectURL) URL.createObjectURL = undefined; //@ts-expect-error if (!URL.revokeObjectURL) URL.revokeObjectURL = undefined; + +const simulatedEventTarget = Object.create(EventTarget.prototype); +simulatedEventTarget.addEventListener = vi.fn(); +simulatedEventTarget.removeEventListener = vi.fn(); +simulatedEventTarget.dispatchEvent = vi.fn(); +vi.stubGlobal("simulatedEventTarget", simulatedEventTarget); From 20b56fc188df235a4520faf64a418c472c0906d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Fri, 30 Jan 2026 21:26:50 +0800 Subject: [PATCH 22/33] =?UTF-8?q?=E5=88=86=E7=A6=BB=E5=B8=B8=E9=87=8F?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8DSW=E7=8E=AF=E5=A2=83=E6=97=A0CustomE?= =?UTF-8?q?vent=E7=9A=84=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 | 14 -------------- packages/message/consts.ts | 13 +++++++++++++ src/app/service/content/utils.ts | 2 +- src/content.ts | 2 +- src/inject.ts | 2 +- src/scripting.ts | 2 +- 6 files changed, 17 insertions(+), 18 deletions(-) create mode 100644 packages/message/consts.ts diff --git a/packages/message/common.ts b/packages/message/common.ts index 2c05f0e67..86a4c1ede 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -1,17 +1,3 @@ -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; diff --git a/packages/message/consts.ts b/packages/message/consts.ts new file mode 100644 index 000000000..aea4ef1d0 --- /dev/null +++ b/packages/message/consts.ts @@ -0,0 +1,13 @@ +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; diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index 83b458f02..6fe01c634 100644 --- a/src/app/service/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -3,7 +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"; +import { ScriptEnvTag } from "@Packages/message/consts"; export type CompileScriptCodeResource = { name: string; diff --git a/src/content.ts b/src/content.ts index e0a94e43c..263fcb207 100644 --- a/src/content.ts +++ b/src/content.ts @@ -2,7 +2,7 @@ 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, pageDispatchEvent } from "@Packages/message/common"; -import { ScriptEnvTag } from "@Packages/message/common"; +import { ScriptEnvTag } from "@Packages/message/consts"; import { uuidv5 } from "./pkg/utils/uuid"; import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_executor"; import type { ValueUpdateDataEncoded } from "./app/service/content/types"; diff --git a/src/inject.ts b/src/inject.ts index 7e25fef8e..3526f96a4 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -2,7 +2,7 @@ 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, pageDispatchEvent } from "@Packages/message/common"; -import { ScriptEnvTag } from "@Packages/message/common"; +import { ScriptEnvTag } from "@Packages/message/consts"; import { uuidv5 } from "./pkg/utils/uuid"; import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_executor"; import type { ValueUpdateDataEncoded } from "./app/service/content/types"; diff --git a/src/scripting.ts b/src/scripting.ts index 1794344e9..7d8c22c9f 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -2,7 +2,6 @@ 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"; @@ -11,6 +10,7 @@ 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"; +import { ScriptEnvTag, ScriptEnvType } from "@Packages/message/consts"; //@ts-ignore const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY); From 87c0a19dea9cad2e9ebdce13bb4d998ea386ad74 Mon Sep 17 00:00:00 2001 From: wangyizhi Date: Sat, 31 Jan 2026 09:48:26 +0800 Subject: [PATCH 23/33] =?UTF-8?q?=E9=87=8D=E6=9E=84=20#1067=20(#1162)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 处理service worker没有MouseEvent的问题 * wip * wip * wip * 重构消息机制 * 修复type问题 * 解决冲突 * 单元测试 * ScriptCat代碼 不使用 EventListenerObject * 刪無關Debug代碼 * 刪未使用 getMessageFlag() * 注釋修訂 * 统一写法降低维护成本 * ScriptCat代碼 不使用 EventListenerObject * revised negotiateEventFlag * Update common.ts * 修复flag顺序导致的协商问题 * 调整单元测试参数位置 * vitest env fix * Revert: 调整单元测试参数位置 * lint * Revert: 调整单元测试参数位置 * 調整代碼 * 加入 readyDeferred 和 isReady * fix * fix unit test * 调整单元测试 * 调整单元测试 * 单元测试 * 单元测试 * 统一大小写 * 刪無用代碼 * 抽取至共通 ReadyWrap * ReadyWrap 釋放已使用 resolve, promise * 删除debug日志和调整测试 * 调整日志等级 * 修复测试脚本GM log使用问题 --------- Co-authored-by: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> --- example/tests/early_inject_content_test.js | 148 +++++---- example/tests/early_test.js | 146 +++++---- example/tests/inject_content_test.js | 2 +- packages/message/common.ts | 74 ++++- packages/message/custom_event_message.ts | 82 ++--- packages/message/server.test.ts | 60 +--- src/app/service/content/gm_api/gm_api.ts | 1 + src/app/service/content/script_executor.ts | 8 +- src/app/service/content/script_runtime.ts | 98 ++++++ src/app/service/content/scripting.ts | 168 ++++++++++ src/app/service/service_worker/runtime.ts | 25 +- src/app/service/service_worker/value.ts | 2 +- src/content.ts | 195 ++---------- src/inject.ts | 285 ++--------------- src/pkg/utils/ready-wrap.ts | 16 + src/scripting.ts | 338 ++------------------- tests/vitest.setup.ts | 6 - 17 files changed, 639 insertions(+), 1015 deletions(-) create mode 100644 src/app/service/content/script_runtime.ts create mode 100644 src/app/service/content/scripting.ts create mode 100644 src/pkg/utils/ready-wrap.ts diff --git a/example/tests/early_inject_content_test.js b/example/tests/early_inject_content_test.js index b3f9474cf..1abfa9803 100644 --- a/example/tests/early_inject_content_test.js +++ b/example/tests/early_inject_content_test.js @@ -18,47 +18,93 @@ // @run-at document-start // ==/UserScript== -(async function () { - "use strict"; - - console.log("%c=== Content环境 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;"); - - let testResults = { - passed: 0, - failed: 0, - total: 0, - }; - - // 测试辅助函数(支持同步和异步) - async function test(name, fn) { - testResults.total++; - try { - await fn(); - testResults.passed++; - console.log(`%c✓ ${name}`, "color: green;"); - return true; - } catch (error) { - testResults.failed++; - console.error(`%c✗ ${name}`, "color: red;", error); - return false; - } +// 测试辅助函数(支持同步和异步) +async function test(name, fn) { + testResults.total++; + try { + await fn(); + testResults.passed++; + console.log(`%c✓ ${name}`, "color: green;"); + return true; + } catch (error) { + testResults.failed++; + console.error(`%c✗ ${name}`, "color: red;", error); + return false; } - - // assert(expected, actual, message) - 比较两个值是否相等 - function assert(expected, actual, message) { - if (expected !== actual) { - const valueInfo = `期望 ${JSON.stringify(expected)}, 实际 ${JSON.stringify(actual)}`; - const error = message ? `${message} - ${valueInfo}` : `断言失败: ${valueInfo}`; - throw new Error(error); - } +} + +function testSync(name, fn) { + testResults.total++; + try { + fn(); + testResults.passed++; + console.log(`%c✓ ${name}`, "color: green;"); + return true; + } catch (error) { + testResults.failed++; + console.error(`%c✗ ${name}`, "color: red;", error); + return false; } +} + +// assert(expected, actual, message) - 比较两个值是否相等 +function assert(expected, actual, message) { + if (expected !== actual) { + const valueInfo = `期望 ${JSON.stringify(expected)}, 实际 ${JSON.stringify(actual)}`; + const error = message ? `${message} - ${valueInfo}` : `断言失败: ${valueInfo}`; + throw new Error(error); + } +} - // assertTrue(condition, message) - 断言条件为真 - function assertTrue(condition, message) { - if (!condition) { - throw new Error(message || "断言失败: 期望条件为真"); - } +// assertTrue(condition, message) - 断言条件为真 +function assertTrue(condition, message) { + if (!condition) { + throw new Error(message || "断言失败: 期望条件为真"); } +} + +console.log("%c=== Content环境 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;"); + +let testResults = { + passed: 0, + failed: 0, + total: 0, +}; + +// 同步测试 + +// ============ GM_addElement/GM_addStyle 测试 ============ +console.log("\n%c--- DOM操作 API 测试 ---", "color: orange; font-weight: bold;"); + +testSync("GM_addElement", () => { + const element = GM_addElement("div", { + textContent: "GM_addElement测试元素", + style: "display:none;", + id: "gm-test-element", + }); + assertTrue(element !== null && element !== undefined, "GM_addElement应该返回元素"); + assert("gm-test-element", element.id, "元素ID应该正确"); + assert("DIV", element.tagName, "元素标签应该是DIV"); + console.log("返回元素:", element); + // 清理测试元素 + element.parentNode.removeChild(element); +}); + +testSync("GM_addStyle", () => { + const styleElement = GM_addStyle(` + .gm-style-test { + color: #10b981 !important; + } + `); + assertTrue(styleElement !== null && styleElement !== undefined, "GM_addStyle应该返回样式元素"); + assertTrue(styleElement.tagName === "STYLE" || styleElement.sheet, "应该返回STYLE元素或样式对象"); + console.log("返回样式元素:", styleElement); + // 清理测试样式 + styleElement.parentNode.removeChild(styleElement); +}); + +(async function () { + "use strict"; // ============ 早期脚本环境检查 ============ console.log("\n%c--- 早期脚本环境检查 ---", "color: orange; font-weight: bold;"); @@ -107,37 +153,11 @@ console.log("脚本注入到:", node.tagName); }); - // ============ GM_addElement/GM_addStyle 测试 ============ - console.log("\n%c--- DOM操作 API 测试 ---", "color: orange; font-weight: bold;"); - - await test("GM_addElement", () => { - const element = GM_addElement("div", { - textContent: "GM_addElement测试元素", - style: "display:none;", - id: "gm-test-element", - }); - assertTrue(element !== null && element !== undefined, "GM_addElement应该返回元素"); - assert("gm-test-element", element.id, "元素ID应该正确"); - assert("DIV", element.tagName, "元素标签应该是DIV"); - console.log("返回元素:", element); - }); - - await test("GM_addStyle", () => { - const styleElement = GM_addStyle(` - .gm-style-test { - color: #10b981 !important; - } - `); - assertTrue(styleElement !== null && styleElement !== undefined, "GM_addStyle应该返回样式元素"); - assertTrue(styleElement.tagName === "STYLE" || styleElement.sheet, "应该返回STYLE元素或样式对象"); - console.log("返回样式元素:", styleElement); - }); - // ============ GM_log 测试 ============ console.log("\n%c--- GM_log 测试 ---", "color: orange; font-weight: bold;"); await test("GM_log", () => { - GM_log("测试日志输出", { type: "test", value: 123 }); + GM_log("测试日志输出", "info", { type: "test", value: 123 }); // GM_log本身不返回值,只要不抛出异常就算成功 assertTrue(true, "GM_log应该能正常输出"); }); diff --git a/example/tests/early_test.js b/example/tests/early_test.js index 2e835e3e5..9ff6581d7 100644 --- a/example/tests/early_test.js +++ b/example/tests/early_test.js @@ -17,47 +17,91 @@ // @run-at document-start // ==/UserScript== -(async function () { - "use strict"; - - console.log("%c=== 早期脚本 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;"); - - let testResults = { - passed: 0, - failed: 0, - total: 0, - }; - - // 测试辅助函数(支持同步和异步) - async function test(name, fn) { - testResults.total++; - try { - await fn(); - testResults.passed++; - console.log(`%c✓ ${name}`, "color: green;"); - return true; - } catch (error) { - testResults.failed++; - console.error(`%c✗ ${name}`, "color: red;", error); - return false; - } +// 测试辅助函数(支持同步和异步) +async function test(name, fn) { + testResults.total++; + try { + await fn(); + testResults.passed++; + console.log(`%c✓ ${name}`, "color: green;"); + return true; + } catch (error) { + testResults.failed++; + console.error(`%c✗ ${name}`, "color: red;", error); + return false; } - - // assert(expected, actual, message) - 比较两个值是否相等 - function assert(expected, actual, message) { - if (expected !== actual) { - const valueInfo = `期望 ${JSON.stringify(expected)}, 实际 ${JSON.stringify(actual)}`; - const error = message ? `${message} - ${valueInfo}` : `断言失败: ${valueInfo}`; - throw new Error(error); - } +} + +function testSync(name, fn) { + testResults.total++; + try { + fn(); + testResults.passed++; + console.log(`%c✓ ${name}`, "color: green;"); + return true; + } catch (error) { + testResults.failed++; + console.error(`%c✗ ${name}`, "color: red;", error); + return false; } +} + +// assert(expected, actual, message) - 比较两个值是否相等 +function assert(expected, actual, message) { + if (expected !== actual) { + const valueInfo = `期望 ${JSON.stringify(expected)}, 实际 ${JSON.stringify(actual)}`; + const error = message ? `${message} - ${valueInfo}` : `断言失败: ${valueInfo}`; + throw new Error(error); + } +} - // assertTrue(condition, message) - 断言条件为真 - function assertTrue(condition, message) { - if (!condition) { - throw new Error(message || "断言失败: 期望条件为真"); - } +// assertTrue(condition, message) - 断言条件为真 +function assertTrue(condition, message) { + if (!condition) { + throw new Error(message || "断言失败: 期望条件为真"); } +} + +console.log("%c=== 早期脚本 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;"); + +let testResults = { + passed: 0, + failed: 0, + total: 0, +}; + +// ============ GM_addElement/GM_addStyle 测试 ============ +console.log("\n%c--- DOM操作 API 测试 ---", "color: orange; font-weight: bold;"); + +testSync("GM_addElement", () => { + const element = GM_addElement("div", { + textContent: "GM_addElement测试元素", + style: "display:none;", + id: "gm-test-element", + }); + assertTrue(element !== null && element !== undefined, "GM_addElement应该返回元素"); + assert("gm-test-element", element.id, "元素ID应该正确"); + assert("DIV", element.tagName, "元素标签应该是DIV"); + console.log("返回元素:", element); + // 清理测试元素 + element.parentNode.removeChild(element); +}); + +testSync("GM_addStyle", () => { + const styleElement = GM_addStyle(` + .gm-style-test { + color: #10b981 !important; + } + `); + assertTrue(styleElement !== null && styleElement !== undefined, "GM_addStyle应该返回样式元素"); + assertTrue(styleElement.tagName === "STYLE" || styleElement.sheet, "应该返回STYLE元素或样式对象"); + console.log("返回样式元素:", styleElement); + // 清理测试样式 + styleElement.parentNode.removeChild(styleElement); +}); + +(async function () { + "use strict"; // ============ 早期脚本环境检查 ============ console.log("\n%c--- 早期脚本环境检查 ---", "color: orange; font-weight: bold;"); @@ -135,37 +179,11 @@ } }); - // ============ GM_addElement/GM_addStyle 测试 ============ - console.log("\n%c--- DOM操作 API 测试 ---", "color: orange; font-weight: bold;"); - - await test("GM_addElement", () => { - const element = GM_addElement("div", { - textContent: "GM_addElement测试元素", - style: "display:none;", - id: "gm-test-element", - }); - assertTrue(element !== null && element !== undefined, "GM_addElement应该返回元素"); - assert("gm-test-element", element.id, "元素ID应该正确"); - assert("DIV", element.tagName, "元素标签应该是DIV"); - console.log("返回元素:", element); - }); - - await test("GM_addStyle", () => { - const styleElement = GM_addStyle(` - .gm-style-test { - color: #10b981 !important; - } - `); - assertTrue(styleElement !== null && styleElement !== undefined, "GM_addStyle应该返回样式元素"); - assertTrue(styleElement.tagName === "STYLE" || styleElement.sheet, "应该返回STYLE元素或样式对象"); - console.log("返回样式元素:", styleElement); - }); - // ============ GM_log 测试 ============ console.log("\n%c--- GM_log 测试 ---", "color: orange; font-weight: bold;"); await test("GM_log", () => { - GM_log("测试日志输出", { type: "test", value: 123 }); + GM_log("测试日志输出", "info", { type: "test", value: 123 }); // GM_log本身不返回值,只要不抛出异常就算成功 assertTrue(true, "GM_log应该能正常输出"); }); diff --git a/example/tests/inject_content_test.js b/example/tests/inject_content_test.js index c2a2ca981..b16c9d25e 100644 --- a/example/tests/inject_content_test.js +++ b/example/tests/inject_content_test.js @@ -99,7 +99,7 @@ console.log("\n%c--- GM_log 测试 ---", "color: orange; font-weight: bold;"); await test("GM_log", () => { - GM_log("测试日志输出", { type: "test", value: 123 }); + GM_log("测试日志输出", "info", { type: "test", value: 123 }); // GM_log本身不返回值,只要不抛出异常就算成功 assertTrue(true, "GM_log应该能正常输出"); }); diff --git a/packages/message/common.ts b/packages/message/common.ts index 86a4c1ede..bffdd6a40 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -1,8 +1,10 @@ +import { randomMessageFlag } from "@App/pkg/utils/utils"; + // 避免页面载入后改动全域物件导致消息传递失败 export const MouseEventClone = MouseEvent; export const CustomEventClone = CustomEvent; -//@ts-ignore -const performanceClone = process.env.VI_TESTING === "true" ? simulatedEventTarget : performance; + +const performanceClone = (process.env.VI_TESTING === "true" ? new EventTarget() : performance) as Performance; // 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败 export const pageDispatchEvent = performanceClone.dispatchEvent.bind(performanceClone); @@ -17,3 +19,71 @@ export const pageDispatchCustomEvent = (eventType: string, detail: any) => { }); return pageDispatchEvent(ev); }; + +// flag协商 +export function negotiateEventFlag(messageFlag: string, readyCount: number, onInit: (eventFlag: string) => void): void { + const eventFlag = randomMessageFlag(); + onInit(eventFlag); + // 监听 inject/scripting 发来的请求 eventFlag 的消息 + let ready = 0; + const fnEventFlagRequestHandler: EventListener = (ev: Event) => { + if (!(ev instanceof CustomEvent)) return; + + switch (ev.detail?.action) { + case "receivedEventFlag": + // 对方已收到 eventFlag + ready += 1; + if (ready >= readyCount) { + // 已收到两个环境的请求,移除监听 + pageRemoveEventListener(messageFlag, fnEventFlagRequestHandler); + } + break; + case "requestEventFlag": + // 广播通信 flag 给 inject/scripting + pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag }); + break; + } + }; + + pageAddEventListener(messageFlag, fnEventFlagRequestHandler); + + // 广播通信 flag 给 inject/scripting + pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag }); +} + +// 获取协商后的 eventFlag +export function getEventFlag(messageFlag: string, onReady: (eventFlag: string) => void) { + let eventFlag = ""; + const fnEventFlagListener: EventListener = (ev: Event) => { + if (!(ev instanceof CustomEvent)) return; + if (ev.detail?.action != "broadcastEventFlag") return; + eventFlag = ev.detail.eventFlag; + pageRemoveEventListener(messageFlag, fnEventFlagListener); + // 告知对方已收到 eventFlag + pageDispatchCustomEvent(messageFlag, { action: "receivedEventFlag" }); + onReady(eventFlag); + }; + + pageAddEventListener(messageFlag, fnEventFlagListener); + + // 基于同步机制,判断是否已经收到 eventFlag + // 如果没有收到,则主动请求一次 + if (!eventFlag) { + pageDispatchCustomEvent(messageFlag, { action: "requestEventFlag" }); + } +} + +export const createMouseEvent = + process.env.VI_TESTING === "true" + ? (type: string, eventInitDict?: MouseEventInit | undefined): MouseEvent => { + const ev = new MouseEventClone(type, eventInitDict); + eventInitDict = eventInitDict || {}; + for (const [key, value] of Object.entries(eventInitDict)) { + //@ts-ignore + if (ev[key] === undefined) ev[key] = value; + } + return ev; + } + : (type: string, eventInitDict?: MouseEventInit | undefined): MouseEvent => { + return new MouseEventClone(type, eventInitDict); + }; diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index 3338a1716..5943bb632 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -6,11 +6,12 @@ import { DefinedFlags } from "@App/app/service/service_worker/runtime.consts"; import { pageDispatchEvent, pageAddEventListener, - pageRemoveEventListener, pageDispatchCustomEvent, MouseEventClone, CustomEventClone, + createMouseEvent, } from "@Packages/message/common"; +import { ReadyWrap } from "@App/pkg/utils/ready-wrap"; // 避免页面载入后改动 Map.prototype 导致消息传递失败 const relatedTargetMap = new Map(); @@ -29,71 +30,38 @@ export class CustomEventPostMessage implements PostMessage { } } -export type PageMessaging = { - et: string; - bindReceiver?: () => void; - onReady?: (callback: () => any) => any; - setMessageTag: (tag: string) => void; - clearMessageTag: () => void; -}; - -export const createPageMessaging = (et: string) => { - const pageMessaging = { et } as PageMessaging; - let resolveFn: ((value: void | PromiseLike) => void) | null = null; - let promise = et - ? null - : new Promise((resolve) => { - resolveFn = resolve; - }); - pageMessaging.onReady = (callback: () => any) => { - if (pageMessaging.et) { - callback(); - } else { - promise?.then(callback); - } - }; - pageMessaging.setMessageTag = function (tag: string) { - if (this.et) throw new Error("pageMessaging.et has already been set."); - this.et = tag; - resolveFn?.(); - promise = null; - }; - pageMessaging.clearMessageTag = function () { - this.et = ""; - }; - 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(); + readyWrap: ReadyWrap = new ReadyWrap(); constructor( - private pageMessaging: PageMessaging, + messageFlag: string, protected readonly isInbound: boolean ) { - 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) { + this.receiveFlag = `${messageFlag}${isInbound ? DefinedFlags.inboundFlag : DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`; + this.sendFlag = `${messageFlag}${isInbound ? DefinedFlags.outboundFlag : DefinedFlags.inboundFlag}${DefinedFlags.domEvent}`; + pageAddEventListener(this.receiveFlag, (event: Event) => { + if (event instanceof MouseEventClone && event.movementX === 0 && event.cancelable) { + event.preventDefault(); // 告知另一端这边已准备好 + this.readyWrap.setReady(); // 两端已准备好,则 setReady() + } else if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) { relatedTargetMap.set(event.movementX, event.relatedTarget); } else if (event instanceof CustomEventClone) { this.messageHandle(event.detail, new CustomEventPostMessage(this)); } - }; - } - - bindReceiver() { - if (!this.pageMessaging.et) throw new Error("bindReceiver() failed"); - const receiveFlag = `evt_${this.pageMessaging.et}_${this.receiveFlag}`; - pageRemoveEventListener(receiveFlag, this.pageMessagingHandler); // 避免重复 - pageAddEventListener(receiveFlag, this.pageMessagingHandler); + }); + const ev = createMouseEvent(this.sendFlag, { + movementX: 0, + cancelable: true, + }); + // 如另一端已准备好,则 setReady() + if (pageDispatchEvent(ev) === false) this.readyWrap.setReady(); } messageHandle(data: WindowMessageBody, target: PostMessage) { @@ -137,7 +105,7 @@ export class CustomEventMessage implements Message { connect(data: TMessage): Promise { return new Promise((resolve) => { - this.pageMessaging.onReady!(() => { + this.readyWrap.onReady(() => { const body: WindowMessageBody = { messageId: uuidv4(), type: "connect", @@ -151,13 +119,13 @@ export class CustomEventMessage implements Message { } nativeSend(detail: any) { - if (!this.pageMessaging.et) throw new Error("scripting.js is not ready or destroyed."); - pageDispatchCustomEvent(`evt_${this.pageMessaging.et}_${this.sendFlag}`, detail); + if (!this.readyWrap.isReady) throw new Error("custom_event_message is not ready."); + pageDispatchCustomEvent(this.sendFlag, detail); } sendMessage(data: TMessage): Promise { return new Promise((resolve: ((value: T) => void) | null) => { - this.pageMessaging.onReady!(() => { + this.readyWrap.onReady(() => { const messageId = uuidv4(); const body: WindowMessageBody = { messageId, @@ -179,7 +147,7 @@ export class CustomEventMessage implements Message { // 与content页的消息通讯实际是同步,此方法不需要经过background // 但是请注意中间不要有promise syncSendMessage(data: TMessage): TMessage { - if (!this.pageMessaging.et) throw new Error("scripting.js is not ready or destroyed."); + if (!this.readyWrap.isReady) throw new Error("custom_event_message is not ready."); const messageId = uuidv4(); const body: WindowMessageBody = { messageId, @@ -199,12 +167,12 @@ export class CustomEventMessage implements Message { } sendRelatedTarget(target: EventTarget): number { - if (!this.pageMessaging.et) throw new Error("scripting.js is not ready or destroyed."); + if (!this.readyWrap.isReady) throw new Error("custom_event_message is not ready."); // 特殊处理relatedTarget,返回id进行关联 // 先将relatedTarget转换成id发送过去 const id = (relateId = relateId === maxInteger ? 1 : relateId + 1); // 可以使用此种方式交互element - const ev = new MouseEventClone(`evt_${this.pageMessaging.et}_${this.sendFlag}`, { + const ev = createMouseEvent(this.sendFlag, { movementX: id, relatedTarget: target, }); diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index b4f0a4a1a..f59b00abb 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -1,8 +1,7 @@ import { describe, expect, it, beforeEach, vi, afterEach } from "vitest"; import { GetSenderType, SenderConnect, SenderRuntime, Server, type IGetSender } from "./server"; -import { createPageMessaging, CustomEventMessage } from "./custom_event_message"; +import { 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 inboundMessage: CustomEventMessage; @@ -13,60 +12,17 @@ let client: CustomEventMessage; const nextTick = () => Promise.resolve().then(() => {}); const setupGlobal = () => { - const testFlag = uuidv4(); - const testPageMessaging = createPageMessaging(testFlag); + const testFlag = `${uuidv4()}::server.test`; + // 创建 scripting 和 inject / content 之间的消息通道 - inboundMessage = new CustomEventMessage(testPageMessaging, true); // scripting 端 - outboundMessage = new CustomEventMessage(testPageMessaging, false); // inject / content 端 - inboundMessage.bindReceiver(); - outboundMessage.bindReceiver(); + inboundMessage = new CustomEventMessage(testFlag, true); // scripting 端 + outboundMessage = new CustomEventMessage(testFlag, false); // inject / content 端 // 服务端使用 scripting 消息 server = new Server("api", inboundMessage); // 客户端使用 inject / content 消息 client = outboundMessage; - - //@ts-ignore - const window = simulatedEventTarget; - - // 模拟消息传递 - 从 inject 到 content - window.dispatchEvent.mockImplementation((event: Event) => { - if (event instanceof CustomEvent) { - const eventType = event.type; - if (eventType.includes(testFlag)) { - let targetEventType: string; - let messageThis: CustomEventMessage; - let messageThat: CustomEventMessage; - // 根据事件类型确定目标消息处理器 - if (eventType.includes(DefinedFlags.inboundFlag)) { - // inject -> content - targetEventType = eventType.replace(DefinedFlags.inboundFlag, DefinedFlags.outboundFlag); - messageThis = inboundMessage; - messageThat = outboundMessage; - } else if (eventType.includes(DefinedFlags.outboundFlag)) { - // content -> inject - targetEventType = eventType.replace(DefinedFlags.outboundFlag, DefinedFlags.inboundFlag); - messageThis = outboundMessage; - messageThat = inboundMessage; - } else { - throw new Error("test mock failed"); - } - nextTick().then(() => { - messageThis.messageHandle(event.detail, { - postMessage: (data: any) => { - // 响应 - const responseEvent = new CustomEvent(targetEventType, { detail: data }); - messageThat.messageHandle(responseEvent.detail, { - postMessage: vi.fn(), - }); - }, - }); - }); - } - } - return true; - }); }; beforeEach(() => { @@ -257,7 +213,7 @@ describe("Server", () => { }); it.concurrent("应该自动为 Group 名称添加斜杠", async () => { - const mockHandler = vi.fn().mockResolvedValue("auto slash response"); + const mockHandler = vi.fn().mockResolvedValue("nested response"); // 测试不带斜杠的情况 const group1 = server.group("slash-group1"); @@ -661,10 +617,10 @@ describe("Server", () => { it.concurrent("应该能够处理空参数", async () => { const mockHandler = vi.fn().mockResolvedValue("empty response"); - server.on("on-test-empty", mockHandler); + server.on("on-empty", mockHandler); const response = await client.sendMessage({ - action: "api/on-test-empty", + action: "api/on-empty", data: null, }); diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index 18c28f7ba..b34ba1581 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -685,6 +685,7 @@ export default class GMApi extends GM_Base { { textContent: css, }, + isContent, ], }, }); diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index c2796039b..68759420b 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -6,8 +6,9 @@ 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 { pageAddEventListener, pageDispatchEvent, type ScriptEnvTag } from "@Packages/message/common"; +import { pageAddEventListener, pageDispatchEvent } from "@Packages/message/common"; import { isUrlExcluded } from "@App/pkg/utils/match"; +import type { ScriptEnvTag } from "@Packages/message/consts"; export type ExecScriptEntry = { scriptLoadInfo: TScriptInfo; @@ -91,7 +92,7 @@ export class ScriptExecutor { const envLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.envLoadComplete}`; // 监听 脚本加载 // 适用于此「通知环境加载完成」代码执行后的脚本加载 - pageAddEventListener(scriptLoadCompleteEvtName, (ev) => { + const scriptLoadCompleteHandler: EventListener = (ev: Event) => { const detail = (ev as CustomEvent).detail as { scriptFlag: string; scriptInfo: ScriptLoadInfo; @@ -116,7 +117,8 @@ export class ScriptExecutor { } this.execEarlyScript(scriptFlag, detail.scriptInfo, envInfo); } - }); + }; + pageAddEventListener(scriptLoadCompleteEvtName, scriptLoadCompleteHandler); // 通知 环境 加载完成 // 适用于此「通知环境加载完成」代码执行前的脚本加载 const ev = new CustomEvent(envLoadCompleteEvtName); diff --git a/src/app/service/content/script_runtime.ts b/src/app/service/content/script_runtime.ts new file mode 100644 index 000000000..93954a77f --- /dev/null +++ b/src/app/service/content/script_runtime.ts @@ -0,0 +1,98 @@ +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 { initEnvInfo, 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"; +import type { ScriptEnvTag } from "@Packages/message/consts"; + +export class ScriptRuntime { + constructor( + private readonly scripEnvTag: ScriptEnvTag, + private readonly server: Server, + private readonly msg: Message, + private readonly scriptExecutor: ScriptExecutor, + private readonly messageFlag: string + ) {} + + init() { + this.server.on("runtime/emitEvent", (data: EmitEventRequest) => { + // 转发给脚本 + this.scriptExecutor.emitEvent(data); + }); + this.server.on("runtime/valueUpdate", (data: ValueUpdateDataEncoded) => { + this.scriptExecutor.valueUpdate(data); + }); + + this.server.on("pageLoad", (data: { scripts: TScriptInfo[]; envInfo: GMInfoEnv }) => { + // 监听事件 + this.startScripts(data.scripts, data.envInfo); + }); + + // 检查early-start的脚本 + this.scriptExecutor.checkEarlyStartScript(this.scripEnvTag, initEnvInfo); + } + + startScripts(scripts: TScriptInfo[], envInfo: GMInfoEnv) { + this.scriptExecutor.startScripts(scripts, envInfo); + } + + 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/scripting.ts b/src/app/service/content/scripting.ts new file mode 100644 index 000000000..a8e5eb9e2 --- /dev/null +++ b/src/app/service/content/scripting.ts @@ -0,0 +1,168 @@ +import { Client, sendMessage } 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 { RuntimeClient } from "../service_worker/client"; +import { makeBlobURL } from "@App/pkg/utils/utils"; +import type { Logger } from "@App/app/repo/logger"; +import LoggerCore from "@App/app/logger/core"; +import type { ValueUpdateDataEncoded } from "./types"; + +// scripting页的处理 +export default class ScriptingRuntime { + constructor( + // 监听来自service_worker的消息 + private readonly extServer: Server, + // 监听来自inject的消息 + private readonly server: Server, + // 发送给扩展service_worker的通信接口 + private readonly senderToExt: MessageSend, + // 发送给 content的消息接口 + private readonly senderToContent: CustomEventMessage, + // 发送给inject的消息接口 + private readonly senderToInject: CustomEventMessage + ) {} + + // 广播消息给 content 和 inject + broadcastToPage(action: string, data?: any): Promise { + return Promise.all([ + sendMessage(this.senderToContent, "content/" + action, data), + sendMessage(this.senderToInject, "inject/" + action, data), + ]).then(() => undefined); + } + + init() { + this.extServer.on("runtime/emitEvent", (data) => { + // 转发给inject和content + return this.broadcastToPage("runtime/emitEvent", data); + }); + this.extServer.on("runtime/valueUpdate", (data) => { + // 转发给inject和content + return this.broadcastToPage("runtime/valueUpdate", data); + }); + this.server.on("logger", (data: Logger) => { + LoggerCore.logger().log(data.level, data.message, data.label); + }); + + // ================================ + // 来自 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["valueUpdateDelivery"]?.newValue) { + // 转发给 content 和 inject + this.broadcastToPage( + "runtime/valueUpdate", + changes["valueUpdateDelivery"]?.newValue.sendData as ValueUpdateDataEncoded + ); + } + }); + + 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, isContent] = data.params; + let attr = { ...tmpAttr }; + let parentNode: Node | undefined; + + // 根据来源选择不同的消息桥(content / inject) + const msg = isContent ? this.senderToContent : this.senderToInject; + + if (parentNodeId) { + parentNode = msg.getAndDelRelatedTarget(parentNodeId) as Node | undefined; + } + 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() { + const client = new RuntimeClient(this.senderToExt); + // 向service_worker请求脚本列表及环境信息 + client.pageLoad().then((o) => { + if (!o.ok) return; + const { injectScriptList, contentScriptList, envInfo } = o; + + // 向页面 发送脚本列表及环境信息 + if (contentScriptList.length) { + const contentClient = new Client(this.senderToContent, "content"); + // 根据@inject-into content过滤脚本 + contentClient.do("pageLoad", { scripts: contentScriptList, envInfo }); + } + + if (injectScriptList.length) { + const injectClient = new Client(this.senderToInject, "inject"); + // 根据@inject-into content过滤脚本 + injectClient.do("pageLoad", { scripts: injectScriptList, envInfo }); + } + }); + } +} diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index b52bf484d..305bca6db 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -50,7 +50,6 @@ 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"; const ORIGINAL_URLMATCH_SUFFIX = "{ORIGINAL}"; // 用于标记原始URLPatterns的后缀 @@ -111,7 +110,6 @@ export class RuntimeService { sitesLoaded: Set = new Set(); updateSitesBusy: boolean = false; - loadingInitFlagsPromise: Promise | undefined; loadingInitProcessPromise: Promise | undefined; initialCompiledResourcePromise: Promise | undefined; @@ -128,15 +126,6 @@ export class RuntimeService { private scriptDAO: ScriptDAO, private localStorageDAO: LocalStorageDAO ) { - this.loadingInitFlagsPromise = this.localStorageDAO - .get("scriptInjectMessageFlag") - .then((res) => { - runtimeGlobal.messageFlag = res?.value || this.generateMessageFlag(); - if (runtimeGlobal.messageFlag !== res?.value) { - return this.localStorageDAO.save({ key: "scriptInjectMessageFlag", value: runtimeGlobal.messageFlag }); - } - }) - .catch(console.error); this.logger = LoggerCore.logger({ component: "runtime" }); // 使用中间件 @@ -556,11 +545,10 @@ export class RuntimeService { this.initReady = (async () => { // 取得初始值 或 等待各种异步同时进行的初始化 (_1, _2, ...) - const [isUserScriptsAvailable, isLoadScripts, strBlacklist, _1, _2, _3] = await Promise.all([ + const [isUserScriptsAvailable, isLoadScripts, strBlacklist, _1, _2] = await Promise.all([ checkUserScriptsAvailable(), this.systemConfig.getEnableScript(), this.systemConfig.getBlacklist(), - this.loadingInitFlagsPromise, // messageFlag 初始化等待 this.loadingInitProcessPromise, // 初始化程序等待 this.initUserAgentData(), // 初始化:userAgentData ]); @@ -679,25 +667,14 @@ export class RuntimeService { runtimeGlobal.registered = false; // 重置 flag 避免取消注册失败 // 即使注册失败,通过重置 flag 可避免错误地呼叫已取消注册的Script - runtimeGlobal.messageFlag = this.generateMessageFlag(); await Promise.allSettled([ 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 uuidv4(); - } - - getMessageFlag() { - return runtimeGlobal.messageFlag; - } - async buildAndSaveCompiledResourceFromScript(script: Script, withCode: boolean = false) { const scriptRes = withCode ? await this.script.buildScriptRunResource(script) : buildScriptRunResourceBasic(script); const resources = withCode ? scriptRes.resource : await this.resource.getScriptResources(scriptRes, true); diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index 48cf5143a..d1ce2573a 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -79,7 +79,7 @@ export class ValueService { async pushValueToTab(sendData: T) { chrome.storage.local.set({ valueUpdateDelivery: { - rId: `${Date.now()}.${Math.random()}`, + rId: `${Date.now()}.${Math.random()}`, // 用于区分不同的更新,确保 chrome.storage.local.onChanged 必能触发 sendData, }, }); diff --git a/src/content.ts b/src/content.ts index 263fcb207..130de1d13 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,183 +1,32 @@ 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, pageDispatchEvent } from "@Packages/message/common"; +import { CustomEventMessage } from "@Packages/message/custom_event_message"; +import { Server } from "@Packages/message/server"; +import { ScriptExecutor } from "./app/service/content/script_executor"; +import type { Message } from "@Packages/message/types"; +import { getEventFlag } from "@Packages/message/common"; +import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; -import { 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"; -//@ts-ignore -const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY); +const messageFlag = process.env.SC_RANDOM_KEY!; -// ================================ -// 常量与全局状态 -// ================================ +getEventFlag(messageFlag, (eventFlag: string) => { + const isContent = typeof chrome.runtime?.sendMessage === "function"; + const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; -// 判断当前是否运行在 USER_SCRIPT 环境 (content环境) -const isContent = typeof chrome.runtime?.sendMessage === "function"; -const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; + const msg: Message = new CustomEventMessage(`${eventFlag}${scriptEnvTag}`, false); -// 用于通知页面:content executor 已准备好 -const executorEnvReadyKey = uuidv5("scriptcat-executor-ready", MessageFlag); - -// 页面通信通道(event token 会在握手后设置) -const scriptingMessaging = createPageMessaging(""); // injectFlagEvt -const pageMessaging = createPageMessaging(""); // `${injectFlagEvt}_${scriptEnvTag}` - -// scripting <-> content 的双向消息桥 -const msg = new CustomEventMessage(pageMessaging, false); - -// 日志系统(仅在 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; -}; - -// 重置所有页面通信 token(用于反注册脚本) -const resetMessagingTokens = () => { - scriptingMessaging.clearMessageTag(); - pageMessaging.clearMessageTag(); -}; - -// 根据 injectFlagEvt 设置双方通信 token(仅允许调用一次) -const setMessagingTokens = (injectFlagEvt: string) => { - scriptingMessaging.setMessageTag(injectFlagEvt); - pageMessaging.setMessageTag(`${injectFlagEvt}_${scriptEnvTag}`); -}; - -// 通知 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; - } - - 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": { - // 页面加载完成,启动匹配的脚本 - 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; - } -}; - -// ================================ -// 页面通信绑定与握手 -// ================================ - -// 监听 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; - - 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); + // 初始化日志组件 + const logger = new LoggerCore({ + writer: new MessageWriter(msg, "scripting/logger"), + consoleLevel: process.env.NODE_ENV === "development" ? "debug" : "none", // 只让日志在scripting环境中打印 + labels: { env: "content", href: window.location.href }, }); -}; - -// ================================ -// 启动流程 -// ================================ -// 检查 early-start 脚本 -scriptExecutor.checkEarlyStartScript(scriptEnvTag, initEnvInfo); + logger.logger().debug("content start"); -// 建立握手与通信绑定 -setupHandshake(); - -// 主动触发 ready 事件,请求 scripting 建立连接 -pageDispatchEvent(new CustomEvent(executorEnvReadyKey)); + const server = new Server("content", msg); + const scriptExecutor = new ScriptExecutor(msg); + const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, messageFlag); + runtime.init(); +}); diff --git a/src/inject.ts b/src/inject.ts index 3526f96a4..3350b866a 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -1,272 +1,35 @@ 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, pageDispatchEvent } from "@Packages/message/common"; -import { ScriptEnvTag } from "@Packages/message/consts"; -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 { CustomEventMessage } from "@Packages/message/custom_event_message"; +import { Server } from "@Packages/message/server"; +import { ScriptExecutor } from "./app/service/content/script_executor"; import type { Message } from "@Packages/message/types"; -import { sendMessage } from "@Packages/message/client"; -import { ExternalWhitelist } from "@App/app/const"; - -//@ts-ignore -const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY); - -// ================================ -// 常量与全局状态 -// ================================ - -// 判断当前是否运行在 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); - -// 页面通信通道(event token 会在握手后设置) -const scriptingMessaging = createPageMessaging(""); // injectFlagEvt -const pageMessaging = createPageMessaging(""); // `${injectFlagEvt}_${scriptEnvTag}` - -// scripting <-> inject 的双向消息桥 -const msg = new CustomEventMessage(pageMessaging, false); - -// 日志系统(仅在 scripting 环境打印) -const logger = new LoggerCore({ - writer: new MessageWriter(msg, "scripting/logger"), - consoleLevel: "none", - labels: { env: "inject", 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; -}; - -// 重置所有页面通信 token(用于反注册脚本) -const resetMessagingTokens = () => { - scriptingMessaging.clearMessageTag(); - pageMessaging.clearMessageTag(); -}; - -// 根据 injectFlagEvt 设置双方通信 token(仅允许调用一次) -const setMessagingTokens = (injectFlagEvt: string) => { - scriptingMessaging.setMessageTag(injectFlagEvt); - pageMessaging.setMessageTag(`${injectFlagEvt}_${scriptEnvTag}`); -}; - -// 通知 scripting 侧:inject 已完成初始化 -const acknowledgeScriptingReady = (injectFlagEvt: string) => { - pageDispatchCustomEvent(injectFlagEvt, { - [`emitterKeyFor${injectFlagEvt}`]: isContent ? 2 : 1, - }); -}; - -// ================================ -// 对外接口:external 注入 -// ================================ - -// 判断当前 hostname 是否命中白名单(含子域名) -function isExternalWhitelisted(hostname: string) { - return ExternalWhitelist.some( - (t) => hostname.endsWith(t) && (hostname.length === t.length || hostname.endsWith(`.${t}`)) - ); -} - -// 生成暴露给页面的 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 { - // 忽略错误 - } - 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); - } -} - -// ================================ -// 消息分发处理 -// ================================ - -// 处理 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); - }); -}; - -// 建立 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(); +import { getEventFlag } from "@Packages/message/common"; +import { ScriptRuntime } from "./app/service/content/script_runtime"; +import { ScriptEnvTag } from "@Packages/message/consts"; - // 初始化通信 token - setMessagingTokens(injectFlagEvt); - msg.bindReceiver(); +const messageFlag = process.env.SC_RANDOM_KEY!; - logger.logger().debug("inject start - init"); +getEventFlag(messageFlag, (eventFlag: string) => { + const isContent = typeof chrome.runtime?.sendMessage === "function"; + const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; - // 建立消息监听 - bindScriptingDeliveryOnce(); + const msg: Message = new CustomEventMessage(`${eventFlag}${scriptEnvTag}`, false); - // 回传 ready 信号 - acknowledgeScriptingReady(injectFlagEvt); + // 加载logger组件 + const logger = new LoggerCore({ + writer: new MessageWriter(msg, "scripting/logger"), + consoleLevel: process.env.NODE_ENV === "development" ? "debug" : "none", // 只让日志在scripting环境中打印 + labels: { env: "inject", href: window.location.href }, }); -}; - -// ================================ -// 启动流程 -// ================================ -// 检查 early-start 脚本 -scriptExecutor.checkEarlyStartScript(scriptEnvTag, initEnvInfo); + logger.logger().debug("inject start"); -// 建立握手与通信绑定 -setupHandshake(); + const server = new Server("inject", msg); + const scriptExecutor = new ScriptExecutor(msg); + const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, messageFlag); + runtime.init(); -// 主动触发 ready 事件,请求 scripting 建立连接 -pageDispatchEvent(new CustomEvent(executorEnvReadyKey)); + // inject环境,直接判断白名单,注入对外接口 + runtime.externalMessage(); +}); diff --git a/src/pkg/utils/ready-wrap.ts b/src/pkg/utils/ready-wrap.ts new file mode 100644 index 000000000..ef2edb36d --- /dev/null +++ b/src/pkg/utils/ready-wrap.ts @@ -0,0 +1,16 @@ +export class ReadyWrap { + public isReady: boolean = false; + private resolve: ((value: void | PromiseLike) => void) | null = null; + private promise: Promise | null = new Promise((resolve) => { + this.resolve = resolve; + }); + onReady(fn: () => any) { + this.isReady ? fn() : this.promise!.then(fn); + } + setReady() { + this.resolve?.(); + this.isReady = true; + this.resolve = null; + this.promise = null; + } +} diff --git a/src/scripting.ts b/src/scripting.ts index 7d8c22c9f..9ff6d0bb1 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -1,315 +1,39 @@ +import { ExtensionMessage } from "@Packages/message/extension_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 { 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"; -import { ScriptEnvTag, ScriptEnvType } from "@Packages/message/consts"; - -//@ts-ignore -const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY); - -// ================================ -// 常量与全局状态 -// ================================ - -// 记录脚本 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`); -}; - -// ================================ -// 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": { - const [url, isContent] = data.params; - return new Promise((resolve) => { - const xhr = new XMLHttpRequest(); - xhr.responseType = "document"; - xhr.open("GET", url); - xhr.onload = () => { - // 根据来源选择不同的消息桥(content / inject) - const msg = isContent ? senderToContent : senderToInject; - const nodeId = msg.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: Node | undefined; - if (parentNodeId) { - parentNode = msg.getAndDelRelatedTarget(parentNodeId) as Node | undefined; - } - - // 创建元素并设置属性 - 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); +import type { Message } from "@Packages/message/types"; +import { CustomEventMessage } from "@Packages/message/custom_event_message"; +import { ScriptEnvTag } from "@Packages/message/consts"; +import { Server } from "@Packages/message/server"; +import ScriptingRuntime from "./app/service/content/scripting"; +import { negotiateEventFlag } from "@Packages/message/common"; + +const messageFlag = process.env.SC_RANDOM_KEY!; + +// 将初始化流程完成后,将EventFlag通知到其他环境 +negotiateEventFlag(messageFlag, 2, (eventFlag) => { + // 建立与service_worker页面的连接 + const extMsgComm: Message = new ExtensionMessage(false); + // 初始化日志组件 + const logger = new LoggerCore({ + writer: new MessageWriter(extMsgComm, "serviceWorker/logger"), + labels: { env: "scripting" }, }); -}; - -// ================================ -// 握手: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.setMessageTag(injectFlagEvt); - scriptExecutorMsgIT.setMessageTag(`${injectFlagEvt}_${ScriptEnvTag.inject}`); - scriptExecutorMsgCT.setMessageTag(`${injectFlagEvt}_${ScriptEnvTag.content}`); + logger.logger().debug("scripting start"); - // 绑定 receiver(允许 inject/content 发消息给 scripting) - scriptExecutorMsgTxIT.bindReceiver(); - scriptExecutorMsgTxCT.bindReceiver(); + const contentMsg = new CustomEventMessage(`${eventFlag}${ScriptEnvTag.content}`, true); + const injectMsg = new CustomEventMessage(`${eventFlag}${ScriptEnvTag.inject}`, true); - // 建立 server:inject/content -> scripting 通道 - const server = new Server("scripting", [scriptExecutorMsgTxIT, scriptExecutorMsgTxCT]); - prepareServer(server, senderToExt, scriptExecutorMsgTxIT, scriptExecutorMsgTxCT); + const server = new Server("scripting", [contentMsg, injectMsg]); - // 建立向页面投递消息的 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 (readyFlag < 3 && 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) { - messageDeliveryToPage.dispatch({ - tag: "localStorage:scriptInjectMessageFlag", - value: changes["localStorage:scriptInjectMessageFlag"]?.newValue, - }); - } - if (changes["valueUpdateDelivery"]?.newValue) { - 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; - messageDeliveryToPage.dispatch({ - tag: action, - value: data, - }); -}); - -// ================================ -// 启动流程 -// ================================ - -// 1) scripting 直接读取 MessageFlag,并开始握手 -onMessageFlagReceived(MessageFlag); - -// 2) 向 service_worker 请求脚本列表及环境信息,并下发给 inject/content -// 向service_worker请求脚本列表及环境信息 -// - 以 ExtensionMessage 形式 从 scripting 发送到 service_worker 再以 Promise 形式取回 service_worker 结果 -client.pageLoad().then((o) => { - if (!o.ok) return; - - // 记录 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, - }); + // Opera中没有chrome.runtime.onConnect,并且content也不需要chrome.runtime.onConnect + // 所以不需要处理连接,设置为false + const extServer = new Server("scripting", extMsgComm, false); + // scriptExecutor的消息接口 + // 初始化运行环境 + const runtime = new ScriptingRuntime(extServer, server, extMsgComm, contentMsg, injectMsg); + runtime.init(); + // 页面加载,注入脚本 + runtime.pageLoad(); }); diff --git a/tests/vitest.setup.ts b/tests/vitest.setup.ts index a4972e4f7..a992f3f32 100644 --- a/tests/vitest.setup.ts +++ b/tests/vitest.setup.ts @@ -227,9 +227,3 @@ vi.stubGlobal("define", "特殊关键字不能穿透沙盒"); if (!URL.createObjectURL) URL.createObjectURL = undefined; //@ts-expect-error if (!URL.revokeObjectURL) URL.revokeObjectURL = undefined; - -const simulatedEventTarget = Object.create(EventTarget.prototype); -simulatedEventTarget.addEventListener = vi.fn(); -simulatedEventTarget.removeEventListener = vi.fn(); -simulatedEventTarget.dispatchEvent = vi.fn(); -vi.stubGlobal("simulatedEventTarget", simulatedEventTarget); From 1d46c8626c60bcd8f55667f08bbd8864728b2c46 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 31 Jan 2026 12:28:24 +0900 Subject: [PATCH 24/33] =?UTF-8?q?chrome.runtime.lastError=20=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/value.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index d1ce2573a..78d11587b 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -77,12 +77,20 @@ export class ValueService { // 推送值到tab async pushValueToTab(sendData: T) { - chrome.storage.local.set({ - valueUpdateDelivery: { - rId: `${Date.now()}.${Math.random()}`, // 用于区分不同的更新,确保 chrome.storage.local.onChanged 必能触发 - sendData, + chrome.storage.local.set( + { + valueUpdateDelivery: { + rId: `${Date.now()}.${Math.random()}`, // 用于区分不同的更新,确保 chrome.storage.local.onChanged 必能触发 + sendData, + }, }, - }); + () => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.runtime.lastError in chrome.storage.local.set", lastError); + } + } + ); // 推送到offscreen中 this.runtime!.sendMessageToTab( { From 7676ece1aa8280ec980ce937b249d84fc1904ff4 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 31 Jan 2026 12:31:21 +0900 Subject: [PATCH 25/33] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=9C=AA=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=20MessageDelivery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/message-delivery.ts | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 src/message-delivery.ts diff --git a/src/message-delivery.ts b/src/message-delivery.ts deleted file mode 100644 index 6cb877bad..000000000 --- a/src/message-delivery.ts +++ /dev/null @@ -1,33 +0,0 @@ -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); - } - } - } - } -} From 2af1592452a72423d0afd50c6d9d1a769dc1ede2 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 31 Jan 2026 12:34:46 +0900 Subject: [PATCH 26/33] =?UTF-8?q?vitest=20=E6=B5=8B=E8=AF=95=E7=94=A8=20SC?= =?UTF-8?q?=5FRANDOM=5FKEY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vitest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vitest.config.ts b/vitest.config.ts index 80250fbf1..1a82bcb1b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -29,6 +29,7 @@ export default defineConfig({ setupFiles: ["./tests/vitest.setup.ts"], env: { VI_TESTING: "true", + SC_RANDOM_KEY: "005a7deb-3a6e-4337-83ea-b9626c02ea38", }, }, }); From 965abdb62135279a9414403ecad958296f3f11d7 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 31 Jan 2026 12:58:36 +0900 Subject: [PATCH 27/33] =?UTF-8?q?//=20=E5=8A=A0=E8=BD=BDlogger=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=20->=20//=20=E5=88=9D=E5=A7=8B=E5=8C=96=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/inject.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inject.ts b/src/inject.ts index 3350b866a..08859563a 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -16,7 +16,7 @@ getEventFlag(messageFlag, (eventFlag: string) => { const msg: Message = new CustomEventMessage(`${eventFlag}${scriptEnvTag}`, false); - // 加载logger组件 + // 初始化日志组件 const logger = new LoggerCore({ writer: new MessageWriter(msg, "scripting/logger"), consoleLevel: process.env.NODE_ENV === "development" ? "debug" : "none", // 只让日志在scripting环境中打印 From 5d00d0c84fad8ec50e9551079aaab45256641d97 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 31 Jan 2026 13:13:31 +0900 Subject: [PATCH 28/33] Update common.ts --- packages/message/common.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index bffdd6a40..400340cb4 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -24,7 +24,7 @@ export const pageDispatchCustomEvent = (eventType: string, detail: any) => { export function negotiateEventFlag(messageFlag: string, readyCount: number, onInit: (eventFlag: string) => void): void { const eventFlag = randomMessageFlag(); onInit(eventFlag); - // 监听 inject/scripting 发来的请求 eventFlag 的消息 + // 监听 inject/content 发来的请求 eventFlag 的消息 let ready = 0; const fnEventFlagRequestHandler: EventListener = (ev: Event) => { if (!(ev instanceof CustomEvent)) return; @@ -39,15 +39,14 @@ export function negotiateEventFlag(messageFlag: string, readyCount: number, onIn } break; case "requestEventFlag": - // 广播通信 flag 给 inject/scripting + // 广播通信 flag 给 inject/content pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag }); break; } }; + // 设置事件,然后广播通信 flag 给 inject/content pageAddEventListener(messageFlag, fnEventFlagRequestHandler); - - // 广播通信 flag 给 inject/scripting pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag }); } @@ -65,12 +64,8 @@ export function getEventFlag(messageFlag: string, onReady: (eventFlag: string) = }; pageAddEventListener(messageFlag, fnEventFlagListener); - - // 基于同步机制,判断是否已经收到 eventFlag - // 如果没有收到,则主动请求一次 - if (!eventFlag) { - pageDispatchCustomEvent(messageFlag, { action: "requestEventFlag" }); - } + // 设置事件,然后对 scripting 请求 flag + pageDispatchCustomEvent(messageFlag, { action: "requestEventFlag" }); } export const createMouseEvent = From efa8a08c6ac4feaf788ea5247afa7a9235421814 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 31 Jan 2026 13:13:51 +0900 Subject: [PATCH 29/33] Update common.ts --- packages/message/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index 400340cb4..799a8526d 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -63,8 +63,8 @@ export function getEventFlag(messageFlag: string, onReady: (eventFlag: string) = onReady(eventFlag); }; - pageAddEventListener(messageFlag, fnEventFlagListener); // 设置事件,然后对 scripting 请求 flag + pageAddEventListener(messageFlag, fnEventFlagListener); pageDispatchCustomEvent(messageFlag, { action: "requestEventFlag" }); } From 9ffee58a4ca4471c887e8e4c9b12e472bdc7a6d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Sat, 31 Jan 2026 15:23:25 +0800 Subject: [PATCH 30/33] =?UTF-8?q?=E5=88=A0=E9=99=A4=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/runtime.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 305bca6db..f48768340 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -667,11 +667,7 @@ export class RuntimeService { runtimeGlobal.registered = false; // 重置 flag 避免取消注册失败 // 即使注册失败,通过重置 flag 可避免错误地呼叫已取消注册的Script - await Promise.allSettled([ - chrome.userScripts?.unregister(), - chrome.scripting.unregisterContentScripts(), - chrome.storage.session.set({ unregisterUserscriptsFlag: `${Date.now()}.${Math.random()}` }), - ]); + await Promise.allSettled([chrome.userScripts?.unregister(), chrome.scripting.unregisterContentScripts()]); } } From 1a4502c56a0decd5e736427283c0d337504c3750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Sat, 31 Jan 2026 15:32:54 +0800 Subject: [PATCH 31/33] =?UTF-8?q?=E4=BF=AE=E5=A4=8DCAT=5FfetchDocument?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/scripting.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/service/content/scripting.ts b/src/app/service/content/scripting.ts index a8e5eb9e2..615a3cc38 100644 --- a/src/app/service/content/scripting.ts +++ b/src/app/service/content/scripting.ts @@ -81,11 +81,15 @@ export default class ScriptingRuntime { } case "CAT_fetchDocument": { return new Promise((resolve) => { + const [url, isContent] = data.params; const xhr = new XMLHttpRequest(); xhr.responseType = "document"; - xhr.open("GET", data.params[0]); + xhr.open("GET", url); xhr.onload = () => { - const nodeId = (this.senderToInject as CustomEventMessage).sendRelatedTarget(xhr.response); + data.params[1]; + // 根据来源选择不同的消息桥(content / inject) + const msg = isContent ? this.senderToContent : this.senderToInject; + const nodeId = msg.sendRelatedTarget(xhr.response); resolve(nodeId); }; xhr.send(); From a8fd5c500f930ac4224ec3fc2e989070f0f3d887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Sat, 31 Jan 2026 15:37:25 +0800 Subject: [PATCH 32/33] =?UTF-8?q?=E6=95=B4=E7=90=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/scripting.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/service/content/scripting.ts b/src/app/service/content/scripting.ts index 615a3cc38..2c9324e21 100644 --- a/src/app/service/content/scripting.ts +++ b/src/app/service/content/scripting.ts @@ -86,7 +86,6 @@ export default class ScriptingRuntime { xhr.responseType = "document"; xhr.open("GET", url); xhr.onload = () => { - data.params[1]; // 根据来源选择不同的消息桥(content / inject) const msg = isContent ? this.senderToContent : this.senderToInject; const nodeId = msg.sendRelatedTarget(xhr.response); From 242d0b5800872a254e985e99c59a95c8a719ea74 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:45:11 +0900 Subject: [PATCH 33/33] =?UTF-8?q?=E6=95=B4=E7=90=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/scripting.ts | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/app/service/content/scripting.ts b/src/app/service/content/scripting.ts index 2c9324e21..17d1b0a84 100644 --- a/src/app/service/content/scripting.ts +++ b/src/app/service/content/scripting.ts @@ -69,7 +69,7 @@ export default class ScriptingRuntime { this.server, this.senderToExt, (data: { api: string; params: any; uuid: string }) => { - // 拦截关注的api + // 拦截关注的 API,未命中则返回 false 交由默认转发处理 switch (data.api) { case "CAT_createBlobUrl": { const file = data.params[0] as File; @@ -80,8 +80,8 @@ export default class ScriptingRuntime { return fetch(data.params[0]).then((res) => res.blob()); } case "CAT_fetchDocument": { + const [url, isContent] = data.params; return new Promise((resolve) => { - const [url, isContent] = data.params; const xhr = new XMLHttpRequest(); xhr.responseType = "document"; xhr.open("GET", url); @@ -96,39 +96,39 @@ export default class ScriptingRuntime { } case "GM_addElement": { const [parentNodeId, tagName, tmpAttr, isContent] = data.params; - let attr = { ...tmpAttr }; - let parentNode: Node | undefined; // 根据来源选择不同的消息桥(content / inject) const msg = isContent ? this.senderToContent : this.senderToInject; + // 取回 parentNode(如果存在) + let parentNode: Node | undefined; if (parentNodeId) { parentNode = msg.getAndDelRelatedTarget(parentNodeId) as Node | undefined; } - const el = document.createElement(tagName); + // 创建元素并设置属性 + const el = document.createElement(tagName); + const attr = tmpAttr ? { ...tmpAttr } : {}; let textContent = ""; - if (attr) { - if (attr.textContent) { - textContent = attr.textContent; - delete attr.textContent; - } - } else { - attr = {}; + 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 || document.head || document.body || document.querySelector("*")).appendChild(el); + 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,所以丢到这里来打印 + // 拦截 GM_log:直接打印到控制台(某些页面可能劫持 console.log) switch (data.params.length) { case 1: console.log(data.params[0]);