From 1459b3941cc06828492dfd36495960ab8eaad854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 22 Jan 2026 16:49:57 +0800 Subject: [PATCH 01/37] =?UTF-8?q?=E5=A4=84=E7=90=86service=20worker?= =?UTF-8?q?=E6=B2=A1=E6=9C=89MouseEvent=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 +++++++++++++ packages/message/custom_event_message.ts | 1 + src/app/service/content/utils.ts | 2 +- src/content.ts | 2 +- src/inject.ts | 2 +- src/scripting.ts | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) create mode 100644 packages/message/consts.ts diff --git a/packages/message/common.ts b/packages/message/common.ts index 51f51b95b..a512f533e 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/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index 3338a1716..b7588b314 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -90,6 +90,7 @@ export class CustomEventMessage implements Message { } bindReceiver() { + console.log("CustomEventMessage bindReceiver", this.pageMessaging.et); if (!this.pageMessaging.et) throw new Error("bindReceiver() failed"); const receiveFlag = `evt_${this.pageMessaging.et}_${this.receiveFlag}`; pageRemoveEventListener(receiveFlag, this.pageMessagingHandler); // 避免重复 diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index 647fc028a..91680d572 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 2d09a844a..4f444cdca 100644 --- a/src/content.ts +++ b/src/content.ts @@ -2,11 +2,11 @@ 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 { 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 { ScriptEnvTag } from "@Packages/message/consts"; //@ts-ignore const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY); diff --git a/src/inject.ts b/src/inject.ts index fc8e04252..0635ead27 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 7ebb2d995..426142eb1 100644 --- a/src/scripting.ts +++ b/src/scripting.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 } from "@Packages/message/common"; -import { ScriptEnvTag, ScriptEnvType } from "@Packages/message/common"; +import { ScriptEnvTag, ScriptEnvType } from "@Packages/message/consts"; import { uuidv5 } from "./pkg/utils/uuid"; import { randomMessageFlag, makeBlobURL } from "@App/pkg/utils/utils"; import { ExtensionMessage } from "@Packages/message/extension_message"; From 5003511ef0d85227f34ac083fd19ddd6518897b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Mon, 26 Jan 2026 18:02:17 +0800 Subject: [PATCH 02/37] wip --- packages/message/custom_event_message.ts | 108 +++++----------- src/app/service/content/content.ts | 149 +++++++++++++++++++++++ src/scripting.ts | 8 +- 3 files changed, 182 insertions(+), 83 deletions(-) create mode 100644 src/app/service/content/content.ts diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index b7588b314..69088f1ce 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -6,7 +6,6 @@ import { DefinedFlags } from "@App/app/service/service_worker/runtime.consts"; import { pageDispatchEvent, pageAddEventListener, - pageRemoveEventListener, pageDispatchCustomEvent, MouseEventClone, CustomEventClone, @@ -29,72 +28,28 @@ 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(); 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) => { + this.receiveFlag = `${messageFlag}${isInbound ? DefinedFlags.inboundFlag : DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`; + this.sendFlag = `${messageFlag}${isInbound ? DefinedFlags.outboundFlag : DefinedFlags.inboundFlag}${DefinedFlags.domEvent}`; + pageAddEventListener(this.receiveFlag, (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)); } - }; - } - - bindReceiver() { - console.log("CustomEventMessage bindReceiver", this.pageMessaging.et); - 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); + }); } messageHandle(data: WindowMessageBody, target: PostMessage) { @@ -138,41 +93,36 @@ export class CustomEventMessage implements Message { connect(data: TMessage): Promise { return new Promise((resolve) => { - 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))); - }); + 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 (!this.pageMessaging.et) throw new Error("scripting.js is not ready or destroyed."); - pageDispatchCustomEvent(`evt_${this.pageMessaging.et}_${this.sendFlag}`, detail); + pageDispatchCustomEvent(this.sendFlag, detail); } sendMessage(data: TMessage): Promise { return new Promise((resolve: ((value: T) => void) | null) => { - 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); + 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); }); } @@ -180,7 +130,6 @@ 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."); const messageId = uuidv4(); const body: WindowMessageBody = { messageId, @@ -200,12 +149,11 @@ export class CustomEventMessage implements Message { } sendRelatedTarget(target: EventTarget): number { - 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); // 可以使用此种方式交互element - const ev = new MouseEventClone(`evt_${this.pageMessaging.et}_${this.sendFlag}`, { + const ev = new MouseEventClone(this.sendFlag, { movementX: id, relatedTarget: target, }); diff --git a/src/app/service/content/content.ts b/src/app/service/content/content.ts new file mode 100644 index 000000000..fefd3ff0e --- /dev/null +++ b/src/app/service/content/content.ts @@ -0,0 +1,149 @@ +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 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: Server, + // 监听来自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: Node | 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) 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(messageFlag: string, 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/scripting.ts b/src/scripting.ts index 426142eb1..a9fec9679 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -1,6 +1,6 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; -import { CustomEventMessage, createPageMessaging } from "@Packages/message/custom_event_message"; +import { CustomEventMessage } from "@Packages/message/custom_event_message"; import { pageAddEventListener, pageDispatchCustomEvent } from "@Packages/message/common"; import { ScriptEnvTag, ScriptEnvType } from "@Packages/message/consts"; import { uuidv5 } from "./pkg/utils/uuid"; @@ -12,8 +12,10 @@ 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); +const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; + +// 与 inject/content 协商消息flag +// 由 content 广播随机后的 flag // ================================ // 常量与全局状态 From 64795b4388ed707159ff1a48bf3b103854185426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Tue, 27 Jan 2026 10:19:08 +0800 Subject: [PATCH 03/37] wip --- src/content.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/content.ts b/src/content.ts index 4f444cdca..1c7cda997 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,15 +1,26 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; -import { CustomEventMessage, createPageMessaging } from "@Packages/message/custom_event_message"; +import { CustomEventMessage } from "@Packages/message/custom_event_message"; import { pageAddEventListener, pageDispatchCustomEvent, pageDispatchEvent } 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 { ScriptEnvTag } from "@Packages/message/consts"; +import { randomMessageFlag } from "./pkg/utils/utils"; -//@ts-ignore -const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY); +const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; + +const EventFlag = randomMessageFlag(); + +// 广播通信 flag 给 inject/scripting +pageDispatchCustomEvent(MessageFlag, { action: MessageFlag, EventFlag }); + +pageAddEventListener(EventFlag, (ev) => { + if (!(ev instanceof CustomEvent)) return; + + if (ev.detail?.action != MessageFlag) return; +}); // ================================ // 常量与全局状态 From dadaff4d10de72bf164c57a828d030da1d7a0d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Tue, 27 Jan 2026 17:43:43 +0800 Subject: [PATCH 04/37] wip --- packages/message/common.ts | 52 ++++++ src/content.ts | 189 +--------------------- src/inject.ts | 273 +------------------------------- src/scripting.ts | 316 +------------------------------------ 4 files changed, 61 insertions(+), 769 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index a512f533e..9bf966bbf 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -16,3 +16,55 @@ export const pageDispatchCustomEvent = (eventType: string, detail: any) => { }); return pageDispatchEvent(ev); }; + +// flag协商 +export function negotiateEventFlag(messageFlag: string, eventFlag: string, readyCount: number = 2): void { + // 广播通信 flag 给 inject/scripting + pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", EventFlag: eventFlag }); + + // 监听 inject/scripting 发来的请求 EventFlag 的消息 + let ready = 0; + const EventFlagRequestHandler: EventListenerOrEventListenerObject = (ev) => { + if (!(ev instanceof CustomEvent)) return; + + switch (ev.detail?.action) { + case "receivedEventFlag": + // 对方已收到 EventFlag + ready += 1; + if (ready >= readyCount) { + // 已收到两个环境的请求,移除监听 + pageRemoveEventListener(messageFlag, EventFlagRequestHandler); + } + break; + case "requestEventFlag": + // 广播通信 flag 给 inject/scripting + pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", EventFlag: eventFlag }); + break; + } + }; + + pageAddEventListener(messageFlag, EventFlagRequestHandler); +} + +// 获取协商后的 EventFlag +export function getEventFlag(messageFlag: string): string { + let eventFlag = ""; + const EventFlagListener: EventListenerOrEventListenerObject = (ev) => { + if (!(ev instanceof CustomEvent)) return; + if (ev.detail?.action != "broadcastEventFlag") return; + eventFlag = ev.detail.EventFlag; + pageRemoveEventListener(messageFlag, EventFlagListener); + // 告知对方已收到 EventFlag + pageDispatchCustomEvent(messageFlag, { action: "receivedEventFlag" }); + }; + + pageAddEventListener(messageFlag, EventFlagListener); + + // 基于同步机制,判断是否已经收到 EventFlag + // 如果没有收到,则主动请求一次 + if (!eventFlag) { + pageDispatchCustomEvent(messageFlag, { action: "requestEventFlag" }); + } + + return eventFlag; +} diff --git a/src/content.ts b/src/content.ts index 1c7cda997..593cd4595 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,194 +1,9 @@ -import LoggerCore from "./app/logger/core"; -import MessageWriter from "./app/logger/message_writer"; -import { CustomEventMessage } from "@Packages/message/custom_event_message"; -import { pageAddEventListener, pageDispatchCustomEvent, pageDispatchEvent } 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 { ScriptEnvTag } from "@Packages/message/consts"; +import { negotiateEventFlag } from "@Packages/message/common"; import { randomMessageFlag } from "./pkg/utils/utils"; const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; const EventFlag = randomMessageFlag(); -// 广播通信 flag 给 inject/scripting -pageDispatchCustomEvent(MessageFlag, { action: MessageFlag, EventFlag }); +negotiateEventFlag(MessageFlag, EventFlag); -pageAddEventListener(EventFlag, (ev) => { - if (!(ev instanceof CustomEvent)) return; - - if (ev.detail?.action != MessageFlag) return; -}); - -// ================================ -// 常量与全局状态 -// ================================ - -// 判断当前是否运行在 USER_SCRIPT 环境 (content环境) -const isContent = typeof chrome.runtime?.sendMessage === "function"; -const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; - -// 用于通知页面: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); - }); -}; - -// ================================ -// 启动流程 -// ================================ - -// 检查 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 0635ead27..e9703ccb8 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -1,272 +1,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/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 type { Message } from "@Packages/message/types"; -import { sendMessage } from "@Packages/message/client"; -import { ExternalWhitelist } from "@App/app/const"; +import { getEventFlag } from "@Packages/message/common"; -//@ts-ignore -const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY); +const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; -// ================================ -// 常量与全局状态 -// ================================ +const EventFlag = getEventFlag(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); - -// 页面通信通道(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(); - - // 初始化通信 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)); +console.log("inject", { MessageFlag, EventFlag }); diff --git a/src/scripting.ts b/src/scripting.ts index a9fec9679..85b5c1ab8 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -1,317 +1,7 @@ -import LoggerCore from "./app/logger/core"; -import MessageWriter from "./app/logger/message_writer"; -import { CustomEventMessage } from "@Packages/message/custom_event_message"; -import { pageAddEventListener, pageDispatchCustomEvent } from "@Packages/message/common"; -import { ScriptEnvTag, ScriptEnvType } from "@Packages/message/consts"; -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 { getEventFlag } from "@Packages/message/common"; const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; -// 与 inject/content 协商消息flag -// 由 content 广播随机后的 flag +const EventFlag = getEventFlag(MessageFlag); -// ================================ -// 常量与全局状态 -// ================================ - -// 记录脚本 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); - }); -}; - -// ================================ -// 握手: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}`); - - // 绑定 receiver(允许 inject/content 发消息给 scripting) - scriptExecutorMsgTxIT.bindReceiver(); - scriptExecutorMsgTxCT.bindReceiver(); - - // 建立 server:inject/content -> scripting 通道 - const server = new Server("scripting", [scriptExecutorMsgTxIT, scriptExecutorMsgTxCT]); - prepareServer(server, senderToExt, scriptExecutorMsgTxIT, scriptExecutorMsgTxCT); - - // 建立向页面投递消息的 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) { - 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, - }); -}); +console.log("scripting", { MessageFlag, EventFlag }); From c584950e6a1325bfa443995cbb84371c31ad38e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Wed, 28 Jan 2026 18:08:46 +0800 Subject: [PATCH 05/37] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/server.test.ts | 9 +- src/app/service/content/script_executor.ts | 8 +- src/app/service/content/script_runtime.ts | 103 ++++++++++++++++++ .../content/{content.ts => scripting.ts} | 91 ++++++++++------ .../service/service_worker/gm_api/gm_api.ts | 1 + src/app/service/service_worker/runtime.ts | 21 +--- src/app/service/service_worker/value.ts | 2 +- src/content.ts | 24 +++- src/inject.ts | 26 ++++- src/scripting.ts | 33 +++++- 10 files changed, 249 insertions(+), 69 deletions(-) create mode 100644 src/app/service/content/script_runtime.ts rename src/app/service/content/{content.ts => scripting.ts} (62%) diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index 603cb6f4e..e81b213d0 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -1,6 +1,6 @@ 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"; @@ -14,12 +14,9 @@ const nextTick = () => Promise.resolve().then(() => {}); const setupGlobal = () => { const testFlag = uuidv4(); - const testPageMessaging = createPageMessaging(testFlag); // 创建 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); diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index 181bbb062..a2b43c232 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 { pageDispatchEvent, type ScriptEnvTag } from "@Packages/message/common"; +import { 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}`; // 监听 脚本加载 // 适用于此「通知环境加载完成」代码执行后的脚本加载 - performance.addEventListener(scriptLoadCompleteEvtName, (ev) => { + const scriptLoadCompleteHandler: EventListenerOrEventListenerObject = (ev) => { const detail = (ev as CustomEvent).detail as { scriptFlag: string; scriptInfo: ScriptLoadInfo; @@ -116,7 +117,8 @@ export class ScriptExecutor { } this.execEarlyScript(scriptFlag, detail.scriptInfo, envInfo); } - }); + }; + performance.addEventListener(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..238215b07 --- /dev/null +++ b/src/app/service/content/script_runtime.ts @@ -0,0 +1,103 @@ +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, this.messageFlag, initEnvInfo); + } + + startScripts(scripts: TScriptInfo[], envInfo: GMInfoEnv) { + this.scriptExecutor.startScripts(scripts, 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/content.ts b/src/app/service/content/scripting.ts similarity index 62% rename from src/app/service/content/content.ts rename to src/app/service/content/scripting.ts index fefd3ff0e..a8e5eb9e2 100644 --- a/src/app/service/content/content.ts +++ b/src/app/service/content/scripting.ts @@ -2,18 +2,14 @@ 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 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"; +import type { ValueUpdateDataEncoded } from "./types"; -// content页的处理 -export default class ContentRuntime { - // 运行在content页面的脚本 - private readonly contentScriptSet: Set = new Set(); - +// scripting页的处理 +export default class ScriptingRuntime { constructor( // 监听来自service_worker的消息 private readonly extServer: Server, @@ -21,27 +17,51 @@ export default class ContentRuntime { private readonly server: Server, // 发送给扩展service_worker的通信接口 private readonly senderToExt: MessageSend, + // 发送给 content的消息接口 + private readonly senderToContent: CustomEventMessage, // 发送给inject的消息接口 - private readonly senderToInject: CustomEventMessage, - // 脚本执行器消息接口 - private readonly scriptExecutorMsg: CustomEventMessage, - private readonly scriptExecutor: ScriptExecutor + 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和scriptExecutor - this.scriptExecutor.emitEvent(data); - return sendMessage(this.senderToInject, "inject/runtime/emitEvent", data); + // 转发给inject和content + return this.broadcastToPage("runtime/emitEvent", data); }); this.extServer.on("runtime/valueUpdate", (data) => { - // 转发给inject和scriptExecutor - this.scriptExecutor.valueUpdate(data); - return sendMessage(this.senderToInject, "inject/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", @@ -72,16 +92,13 @@ export default class ContentRuntime { }); } case "GM_addElement": { - const [parentNodeId, tagName, tmpAttr] = data.params; + const [parentNodeId, tagName, tmpAttr, isContent] = data.params; let attr = { ...tmpAttr }; let parentNode: Node | undefined; - // 判断是不是content脚本发过来的 - let msg: CustomEventMessage; - if (this.contentScriptSet.has(data.uuid) || this.scriptExecutor.execMap.has(data.uuid)) { - msg = this.scriptExecutorMsg; - } else { - msg = this.senderToInject; - } + + // 根据来源选择不同的消息桥(content / inject) + const msg = isContent ? this.senderToContent : this.senderToInject; + if (parentNodeId) { parentNode = msg.getAndDelRelatedTarget(parentNodeId) as Node | undefined; } @@ -127,23 +144,25 @@ export default class ContentRuntime { ); } - pageLoad(messageFlag: string, envInfo: GMInfoEnv) { - this.scriptExecutor.checkEarlyStartScript("content", messageFlag, envInfo); + pageLoad() { 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); + + // 向页面 发送脚本列表及环境信息 + 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 }); } - // 启动脚本 - this.scriptExecutor.startScripts(contentScriptList, envInfo); }); } } diff --git a/src/app/service/service_worker/gm_api/gm_api.ts b/src/app/service/service_worker/gm_api/gm_api.ts index 1e5135605..1100533b9 100644 --- a/src/app/service/service_worker/gm_api/gm_api.ts +++ b/src/app/service/service_worker/gm_api/gm_api.ts @@ -415,6 +415,7 @@ export default class GMApi { @PermissionVerify.API({ link: ["GM_deleteValue", "GM_deleteValues"] }) async GM_setValue(request: GMApiRequest<[string, string, any?]>, sender: IGetSender) { + console.log("GM_setValue", request.params); if (!request.params || request.params.length < 2) { throw new Error("param is failed"); } diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 8e3f8f241..3431b2153 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" }); // 使用中间件 @@ -560,7 +549,6 @@ export class RuntimeService { checkUserScriptsAvailable(), this.systemConfig.getEnableScript(), this.systemConfig.getBlacklist(), - this.loadingInitFlagsPromise, // messageFlag 初始化等待 this.loadingInitProcessPromise, // 初始化程序等待 this.initUserAgentData(), // 初始化:userAgentData ]); @@ -679,23 +667,16 @@ 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; + return process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; } async buildAndSaveCompiledResourceFromScript(script: Script, withCode: boolean = false) { diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index 48cf5143a..00da7f8a7 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()}`, // 用于区分不同的更新 sendData, }, }); diff --git a/src/content.ts b/src/content.ts index 593cd4595..c03aa6cf9 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,5 +1,13 @@ -import { negotiateEventFlag } from "@Packages/message/common"; +import LoggerCore from "./app/logger/core"; +import MessageWriter from "./app/logger/message_writer"; +import { CustomEventMessage } from "@Packages/message/custom_event_message"; +import { Server } from "@Packages/message/server"; +import { ScriptExecutor } from "./app/service/content/script_executor"; import { randomMessageFlag } from "./pkg/utils/utils"; +import type { Message } from "@Packages/message/types"; +import { negotiateEventFlag } from "@Packages/message/common"; +import { ScriptRuntime } from "./app/service/content/script_runtime"; +import { ScriptEnvTag } from "@Packages/message/consts"; const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; @@ -7,3 +15,17 @@ const EventFlag = randomMessageFlag(); negotiateEventFlag(MessageFlag, EventFlag); +const msg: Message = new CustomEventMessage(`${EventFlag}${ScriptEnvTag.content}`, false); + +// 初始化日志组件 +const logger = new LoggerCore({ + writer: new MessageWriter(msg, "scripting/logger"), + labels: { env: "content", href: window.location.href }, +}); + +logger.logger().debug("content start"); + +const server = new Server("content", msg); +const scriptExecutor = new ScriptExecutor(msg); +const runtime = new ScriptRuntime(ScriptEnvTag.content, server, msg, scriptExecutor, MessageFlag); +runtime.init(); diff --git a/src/inject.ts b/src/inject.ts index e9703ccb8..23e1a0393 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -1,7 +1,31 @@ +import LoggerCore from "./app/logger/core"; +import MessageWriter from "./app/logger/message_writer"; +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"; const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; const EventFlag = getEventFlag(MessageFlag); -console.log("inject", { MessageFlag, EventFlag }); +const msg: Message = new CustomEventMessage(`${EventFlag}${ScriptEnvTag.inject}`, false); + +// 加载logger组件 +const logger = new LoggerCore({ + writer: new MessageWriter(msg, "scripting/logger"), + consoleLevel: "none", // 只让日志在scripting环境中打印 + labels: { env: "inject", href: window.location.href }, +}); + +logger.logger().debug("inject start"); + +const server = new Server("inject", msg); +const scriptExecutor = new ScriptExecutor(msg); +const runtime = new ScriptRuntime(ScriptEnvTag.inject, server, msg, scriptExecutor, MessageFlag); +runtime.init(); + +runtime.onInjectPageLoaded(); diff --git a/src/scripting.ts b/src/scripting.ts index 85b5c1ab8..262876c57 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -1,7 +1,38 @@ import { getEventFlag } from "@Packages/message/common"; +import { ExtensionMessage } from "@Packages/message/extension_message"; +import LoggerCore from "./app/logger/core"; +import MessageWriter from "./app/logger/message_writer"; +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"; const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; const EventFlag = getEventFlag(MessageFlag); -console.log("scripting", { MessageFlag, EventFlag }); +// 建立与service_worker页面的连接 +const extMsgComm: Message = new ExtensionMessage(false); +// 初始化日志组件 +const logger = new LoggerCore({ + writer: new MessageWriter(extMsgComm, "serviceWorker/logger"), + labels: { env: "scripting" }, +}); + +logger.logger().debug("scripting start"); + +const contentMsg = new CustomEventMessage(`${EventFlag}${ScriptEnvTag.content}`, true); +const injectMsg = new CustomEventMessage(`${EventFlag}${ScriptEnvTag.inject}`, true); + +const server = new Server("scripting", [contentMsg, injectMsg]); + +// 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(); From 41b28c16a3027a48213c223a981d3b48de008588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Wed, 28 Jan 2026 18:10:49 +0800 Subject: [PATCH 06/37] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dtype=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 ++-- src/app/service/service_worker/runtime.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 315dba9b2..9648bf9cf 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "private": true, "scripts": { "preinstall": "pnpm dlx only-allow pnpm", - "test": "vitest --test-timeout=500 --no-coverage --isolate=false --reporter=verbose", - "test:ci": "vitest run --test-timeout=500 --no-coverage --isolate=false --reporter=default --reporter.summary=false", + "test": "vitest --test-timeout=1000 --no-coverage --isolate=false --reporter=verbose", + "test:ci": "vitest run --test-timeout=1000 --no-coverage --isolate=false --reporter=default --reporter.summary=false", "coverage": "vitest run --coverage", "coverage:ci": "vitest run --coverage --silent --reporter=default --reporter.default.summary=false", "typecheck": "tsc --noEmit", diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 3431b2153..f60234cca 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -545,7 +545,7 @@ 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(), From 2e5821e556c05587c4091f7b7afb0f269a9d8940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Wed, 28 Jan 2026 18:15:39 +0800 Subject: [PATCH 07/37] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/script_runtime.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/app/service/content/script_runtime.ts b/src/app/service/content/script_runtime.ts index 238215b07..93954a77f 100644 --- a/src/app/service/content/script_runtime.ts +++ b/src/app/service/content/script_runtime.ts @@ -32,18 +32,13 @@ export class ScriptRuntime { }); // 检查early-start的脚本 - this.scriptExecutor.checkEarlyStartScript(this.scripEnvTag, this.messageFlag, initEnvInfo); + this.scriptExecutor.checkEarlyStartScript(this.scripEnvTag, initEnvInfo); } startScripts(scripts: TScriptInfo[], envInfo: GMInfoEnv) { this.scriptExecutor.startScripts(scripts, envInfo); } - onInjectPageLoaded() { - // 注入允许外部调用 - this.externalMessage(); - } - externalMessage() { // 对外接口白名单 const hostname = window.location.hostname; From 5724b6fa0a9d377ba0dcd068eea1afd105223cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Wed, 28 Jan 2026 18:19:12 +0800 Subject: [PATCH 08/37] =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9648bf9cf..315dba9b2 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "private": true, "scripts": { "preinstall": "pnpm dlx only-allow pnpm", - "test": "vitest --test-timeout=1000 --no-coverage --isolate=false --reporter=verbose", - "test:ci": "vitest run --test-timeout=1000 --no-coverage --isolate=false --reporter=default --reporter.summary=false", + "test": "vitest --test-timeout=500 --no-coverage --isolate=false --reporter=verbose", + "test:ci": "vitest run --test-timeout=500 --no-coverage --isolate=false --reporter=default --reporter.summary=false", "coverage": "vitest run --coverage", "coverage:ci": "vitest run --coverage --silent --reporter=default --reporter.default.summary=false", "typecheck": "tsc --noEmit", From 62dd049b3f8c11ab0797b8fe3e50473bdfc9ec83 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:31:42 +0900 Subject: [PATCH 09/37] =?UTF-8?q?ScriptCat=E4=BB=A3=E7=A2=BC=20=E4=B8=8D?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20EventListenerObject?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/common.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index 9bf966bbf..9535e9d10 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -24,7 +24,7 @@ export function negotiateEventFlag(messageFlag: string, eventFlag: string, ready // 监听 inject/scripting 发来的请求 EventFlag 的消息 let ready = 0; - const EventFlagRequestHandler: EventListenerOrEventListenerObject = (ev) => { + const EventFlagRequestHandler: EventListener = (ev) => { if (!(ev instanceof CustomEvent)) return; switch (ev.detail?.action) { @@ -49,7 +49,7 @@ export function negotiateEventFlag(messageFlag: string, eventFlag: string, ready // 获取协商后的 EventFlag export function getEventFlag(messageFlag: string): string { let eventFlag = ""; - const EventFlagListener: EventListenerOrEventListenerObject = (ev) => { + const EventFlagListener: EventListener = (ev) => { if (!(ev instanceof CustomEvent)) return; if (ev.detail?.action != "broadcastEventFlag") return; eventFlag = ev.detail.EventFlag; From 9ed89ef795e8de669b30df8d6710472fcca0d414 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:36:29 +0900 Subject: [PATCH 10/37] =?UTF-8?q?=E5=88=AA=E7=84=A1=E9=97=9CDebug=E4=BB=A3?= =?UTF-8?q?=E7=A2=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/gm_api/gm_api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/service/service_worker/gm_api/gm_api.ts b/src/app/service/service_worker/gm_api/gm_api.ts index 1100533b9..1e5135605 100644 --- a/src/app/service/service_worker/gm_api/gm_api.ts +++ b/src/app/service/service_worker/gm_api/gm_api.ts @@ -415,7 +415,6 @@ export default class GMApi { @PermissionVerify.API({ link: ["GM_deleteValue", "GM_deleteValues"] }) async GM_setValue(request: GMApiRequest<[string, string, any?]>, sender: IGetSender) { - console.log("GM_setValue", request.params); if (!request.params || request.params.length < 2) { throw new Error("param is failed"); } From b67ffbd63abc78d3d238b14986d1beb90ba032f7 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:37:43 +0900 Subject: [PATCH 11/37] =?UTF-8?q?=E5=88=AA=E6=9C=AA=E4=BD=BF=E7=94=A8=20ge?= =?UTF-8?q?tMessageFlag()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/runtime.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 9b20383b5..305bca6db 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -675,10 +675,6 @@ export class RuntimeService { } } - getMessageFlag() { - return process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; - } - 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); From 3a5f420f2cdb8a4a2c94b6d579d69323f7bfc0e5 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:39:36 +0900 Subject: [PATCH 12/37] =?UTF-8?q?=E6=B3=A8=E9=87=8B=E4=BF=AE=E8=A8=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/value.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index 00da7f8a7..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, }, }); From a6522e39c9773c7da6ed59c4d14cfd962eb55441 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:43:09 +0900 Subject: [PATCH 13/37] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=86=99=E6=B3=95?= =?UTF-8?q?=E9=99=8D=E4=BD=8E=E7=BB=B4=E6=8A=A4=E6=88=90=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/content.ts | 7 +++++-- src/inject.ts | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/content.ts b/src/content.ts index c03aa6cf9..6cc15bc1b 100644 --- a/src/content.ts +++ b/src/content.ts @@ -15,7 +15,10 @@ const EventFlag = randomMessageFlag(); negotiateEventFlag(MessageFlag, EventFlag); -const msg: Message = new CustomEventMessage(`${EventFlag}${ScriptEnvTag.content}`, false); +const isContent = typeof chrome.runtime?.sendMessage === "function"; +const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; + +const msg: Message = new CustomEventMessage(`${EventFlag}${scriptEnvTag}`, false); // 初始化日志组件 const logger = new LoggerCore({ @@ -27,5 +30,5 @@ logger.logger().debug("content start"); const server = new Server("content", msg); const scriptExecutor = new ScriptExecutor(msg); -const runtime = new ScriptRuntime(ScriptEnvTag.content, server, msg, scriptExecutor, MessageFlag); +const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, MessageFlag); runtime.init(); diff --git a/src/inject.ts b/src/inject.ts index d3937688a..e5ea2ca87 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -12,7 +12,10 @@ const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; const EventFlag = getEventFlag(MessageFlag); -const msg: Message = new CustomEventMessage(`${EventFlag}${ScriptEnvTag.inject}`, false); +const isContent = typeof chrome.runtime?.sendMessage === "function"; +const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; + +const msg: Message = new CustomEventMessage(`${EventFlag}${scriptEnvTag}`, false); // 加载logger组件 const logger = new LoggerCore({ @@ -25,7 +28,7 @@ logger.logger().debug("inject start"); const server = new Server("inject", msg); const scriptExecutor = new ScriptExecutor(msg); -const runtime = new ScriptRuntime(ScriptEnvTag.inject, server, msg, scriptExecutor, MessageFlag); +const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, MessageFlag); runtime.init(); // inject环境,直接判断白名单,注入对外接口 From e870ea4152ba9bd66fb852b030ae35c663efd74a Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:46:19 +0900 Subject: [PATCH 14/37] =?UTF-8?q?ScriptCat=E4=BB=A3=E7=A2=BC=20=E4=B8=8D?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20EventListenerObject?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/script_executor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index d7f3acb81..3b878a8fb 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -92,7 +92,7 @@ export class ScriptExecutor { const envLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.envLoadComplete}`; // 监听 脚本加载 // 适用于此「通知环境加载完成」代码执行后的脚本加载 - const scriptLoadCompleteHandler: EventListenerOrEventListenerObject = (ev) => { + const scriptLoadCompleteHandler: EventListener = (ev) => { const detail = (ev as CustomEvent).detail as { scriptFlag: string; scriptInfo: ScriptLoadInfo; From 6d510a6cf19dd871d56f5f37c10cce03b045dcdb Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:42:15 +0900 Subject: [PATCH 15/37] revised negotiateEventFlag --- packages/message/common.ts | 61 ++++++++++++++++---------------------- src/content.ts | 8 ++--- src/inject.ts | 7 +++-- src/scripting.ts | 7 +++-- 4 files changed, 36 insertions(+), 47 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index 9535e9d10..0e4f64352 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -18,53 +18,42 @@ export const pageDispatchCustomEvent = (eventType: string, detail: any) => { }; // flag协商 -export function negotiateEventFlag(messageFlag: string, eventFlag: string, readyCount: number = 2): void { - // 广播通信 flag 给 inject/scripting - pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", EventFlag: eventFlag }); - +export function negotiateEventFlag(messageFlag: string, firstEventFlag: string, responsedCountMax: number = 3) { + const tag = `${messageFlag}_negotiate`; + let eventFlag = ""; // 监听 inject/scripting 发来的请求 EventFlag 的消息 - let ready = 0; + let responsedCount = 0; const EventFlagRequestHandler: EventListener = (ev) => { if (!(ev instanceof CustomEvent)) return; - switch (ev.detail?.action) { - case "receivedEventFlag": - // 对方已收到 EventFlag - ready += 1; - if (ready >= readyCount) { - // 已收到两个环境的请求,移除监听 - pageRemoveEventListener(messageFlag, EventFlagRequestHandler); + case "responseEventFlag": + if (ev.defaultPrevented) return; + if (eventFlag || !ev.detail?.EventFlag) return; + if (!eventFlag) { + eventFlag = ev.detail?.EventFlag; + if (eventFlag !== firstEventFlag) { + pageRemoveEventListener(tag, EventFlagRequestHandler); + } } break; case "requestEventFlag": - // 广播通信 flag 给 inject/scripting - pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", EventFlag: eventFlag }); + if (ev.defaultPrevented) return; + responsedCount++; + if (responsedCount <= responsedCountMax) { + pageDispatchCustomEvent(tag, { action: "responseEventFlag", EventFlag: firstEventFlag }); + ev.preventDefault(); + ev.stopImmediatePropagation(); + ev.stopPropagation(); + } else { + pageRemoveEventListener(tag, EventFlagRequestHandler); + } break; } }; - - pageAddEventListener(messageFlag, EventFlagRequestHandler); -} - -// 获取协商后的 EventFlag -export function getEventFlag(messageFlag: string): string { - let eventFlag = ""; - const EventFlagListener: EventListener = (ev) => { - if (!(ev instanceof CustomEvent)) return; - if (ev.detail?.action != "broadcastEventFlag") return; - eventFlag = ev.detail.EventFlag; - pageRemoveEventListener(messageFlag, EventFlagListener); - // 告知对方已收到 EventFlag - pageDispatchCustomEvent(messageFlag, { action: "receivedEventFlag" }); - }; - - pageAddEventListener(messageFlag, EventFlagListener); - - // 基于同步机制,判断是否已经收到 EventFlag - // 如果没有收到,则主动请求一次 + pageAddEventListener(tag, EventFlagRequestHandler); + pageDispatchCustomEvent(tag, { action: "requestEventFlag" }); if (!eventFlag) { - pageDispatchCustomEvent(messageFlag, { action: "requestEventFlag" }); + console.error("negotiateEventFlag failed"); } - return eventFlag; } diff --git a/src/content.ts b/src/content.ts index 6cc15bc1b..78e27a320 100644 --- a/src/content.ts +++ b/src/content.ts @@ -3,17 +3,15 @@ import MessageWriter from "./app/logger/message_writer"; import { CustomEventMessage } from "@Packages/message/custom_event_message"; import { Server } from "@Packages/message/server"; import { ScriptExecutor } from "./app/service/content/script_executor"; -import { randomMessageFlag } from "./pkg/utils/utils"; import type { Message } from "@Packages/message/types"; import { negotiateEventFlag } from "@Packages/message/common"; +import { randomMessageFlag } from "./pkg/utils/utils"; import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; -const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; - -const EventFlag = randomMessageFlag(); +const MessageFlag = process.env.SC_RANDOM_KEY; -negotiateEventFlag(MessageFlag, EventFlag); +const EventFlag = negotiateEventFlag(MessageFlag, randomMessageFlag()); const isContent = typeof chrome.runtime?.sendMessage === "function"; const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; diff --git a/src/inject.ts b/src/inject.ts index e5ea2ca87..752f28caf 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -4,13 +4,14 @@ 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 { negotiateEventFlag } from "@Packages/message/common"; +import { randomMessageFlag } from "./pkg/utils/utils"; import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; -const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; +const MessageFlag = process.env.SC_RANDOM_KEY; -const EventFlag = getEventFlag(MessageFlag); +const EventFlag = negotiateEventFlag(MessageFlag, randomMessageFlag()); const isContent = typeof chrome.runtime?.sendMessage === "function"; const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; diff --git a/src/scripting.ts b/src/scripting.ts index 262876c57..12c898e33 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -1,4 +1,3 @@ -import { getEventFlag } from "@Packages/message/common"; import { ExtensionMessage } from "@Packages/message/extension_message"; import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; @@ -7,10 +6,12 @@ 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"; +import { randomMessageFlag } from "./pkg/utils/utils"; -const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; +const MessageFlag = process.env.SC_RANDOM_KEY; -const EventFlag = getEventFlag(MessageFlag); +const EventFlag = negotiateEventFlag(MessageFlag, randomMessageFlag()); // 建立与service_worker页面的连接 const extMsgComm: Message = new ExtensionMessage(false); From 7e9c6b3f4d92b8a00ea04a7b264cec48554f27e7 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:45:27 +0900 Subject: [PATCH 16/37] Update common.ts --- packages/message/common.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index 0e4f64352..4dbe4f7dd 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -21,7 +21,6 @@ export const pageDispatchCustomEvent = (eventType: string, detail: any) => { export function negotiateEventFlag(messageFlag: string, firstEventFlag: string, responsedCountMax: number = 3) { const tag = `${messageFlag}_negotiate`; let eventFlag = ""; - // 监听 inject/scripting 发来的请求 EventFlag 的消息 let responsedCount = 0; const EventFlagRequestHandler: EventListener = (ev) => { if (!(ev instanceof CustomEvent)) return; From 49e87182005adddd3d2253d5a6b9b002ddbcc161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 29 Jan 2026 11:24:36 +0800 Subject: [PATCH 17/37] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dflag=E9=A1=BA=E5=BA=8F?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=E5=8D=8F=E5=95=86=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 | 69 ++++++++++++++---------- src/app/service/content/gm_api/gm_api.ts | 1 + src/content.ts | 36 ++++++------- src/inject.ts | 42 +++++++-------- src/scripting.ts | 48 ++++++++--------- 5 files changed, 106 insertions(+), 90 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index 4dbe4f7dd..dc2d52174 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -1,3 +1,5 @@ +import { randomMessageFlag } from "@App/pkg/utils/utils"; + // 避免页面载入后改动全域物件导致消息传递失败 export const MouseEventClone = MouseEvent; export const CustomEventClone = CustomEvent; @@ -18,41 +20,54 @@ export const pageDispatchCustomEvent = (eventType: string, detail: any) => { }; // flag协商 -export function negotiateEventFlag(messageFlag: string, firstEventFlag: string, responsedCountMax: number = 3) { - const tag = `${messageFlag}_negotiate`; - let eventFlag = ""; - let responsedCount = 0; - const EventFlagRequestHandler: EventListener = (ev) => { +export function negotiateEventFlag(messageFlag: string, readyCount: number, onInit: (eventFlag: string) => void): void { + const eventFlag = randomMessageFlag(); + onInit(eventFlag); + // 监听 inject/scripting 发来的请求 EventFlag 的消息 + let ready = 0; + const EventFlagRequestHandler: EventListenerOrEventListenerObject = (ev) => { if (!(ev instanceof CustomEvent)) return; + switch (ev.detail?.action) { - case "responseEventFlag": - if (ev.defaultPrevented) return; - if (eventFlag || !ev.detail?.EventFlag) return; - if (!eventFlag) { - eventFlag = ev.detail?.EventFlag; - if (eventFlag !== firstEventFlag) { - pageRemoveEventListener(tag, EventFlagRequestHandler); - } + case "receivedEventFlag": + // 对方已收到 EventFlag + ready += 1; + if (ready >= readyCount) { + // 已收到两个环境的请求,移除监听 + pageRemoveEventListener(messageFlag, EventFlagRequestHandler); } break; case "requestEventFlag": - if (ev.defaultPrevented) return; - responsedCount++; - if (responsedCount <= responsedCountMax) { - pageDispatchCustomEvent(tag, { action: "responseEventFlag", EventFlag: firstEventFlag }); - ev.preventDefault(); - ev.stopImmediatePropagation(); - ev.stopPropagation(); - } else { - pageRemoveEventListener(tag, EventFlagRequestHandler); - } + // 广播通信 flag 给 inject/scripting + pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", EventFlag: eventFlag }); break; } }; - pageAddEventListener(tag, EventFlagRequestHandler); - pageDispatchCustomEvent(tag, { action: "requestEventFlag" }); + + pageAddEventListener(messageFlag, EventFlagRequestHandler); + + // 广播通信 flag 给 inject/scripting + pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", EventFlag: eventFlag }); +} + +// 获取协商后的 EventFlag +export function getEventFlag(messageFlag: string, onReady: (eventFlag: string) => void) { + let eventFlag = ""; + const EventFlagListener: EventListenerOrEventListenerObject = (ev) => { + if (!(ev instanceof CustomEvent)) return; + if (ev.detail?.action != "broadcastEventFlag") return; + eventFlag = ev.detail.EventFlag; + pageRemoveEventListener(messageFlag, EventFlagListener); + // 告知对方已收到 EventFlag + pageDispatchCustomEvent(messageFlag, { action: "receivedEventFlag" }); + onReady(eventFlag); + }; + + pageAddEventListener(messageFlag, EventFlagListener); + + // 基于同步机制,判断是否已经收到 EventFlag + // 如果没有收到,则主动请求一次 if (!eventFlag) { - console.error("negotiateEventFlag failed"); + pageDispatchCustomEvent(messageFlag, { action: "requestEventFlag" }); } - return eventFlag; } 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/content.ts b/src/content.ts index 78e27a320..3ed61a187 100644 --- a/src/content.ts +++ b/src/content.ts @@ -4,29 +4,29 @@ 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 { negotiateEventFlag } from "@Packages/message/common"; -import { randomMessageFlag } from "./pkg/utils/utils"; +import { getEventFlag } from "@Packages/message/common"; import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; -const MessageFlag = process.env.SC_RANDOM_KEY; +const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; -const EventFlag = negotiateEventFlag(MessageFlag, randomMessageFlag()); +getEventFlag(MessageFlag, (EventFlag) => { + console.log("content EventFlag", EventFlag); + const isContent = typeof chrome.runtime?.sendMessage === "function"; + const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; -const isContent = typeof chrome.runtime?.sendMessage === "function"; -const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; + const msg: Message = new CustomEventMessage(`${EventFlag}${scriptEnvTag}`, false); -const msg: Message = new CustomEventMessage(`${EventFlag}${scriptEnvTag}`, false); + // 初始化日志组件 + const logger = new LoggerCore({ + writer: new MessageWriter(msg, "scripting/logger"), + labels: { env: "content", href: window.location.href }, + }); -// 初始化日志组件 -const logger = new LoggerCore({ - writer: new MessageWriter(msg, "scripting/logger"), - labels: { env: "content", href: window.location.href }, -}); - -logger.logger().debug("content start"); + logger.logger().debug("content start"); -const server = new Server("content", msg); -const scriptExecutor = new ScriptExecutor(msg); -const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, MessageFlag); -runtime.init(); + 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 752f28caf..9e733d0d8 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -4,33 +4,33 @@ 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 { negotiateEventFlag } from "@Packages/message/common"; -import { randomMessageFlag } from "./pkg/utils/utils"; +import { getEventFlag } from "@Packages/message/common"; import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; -const MessageFlag = process.env.SC_RANDOM_KEY; +const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; -const EventFlag = negotiateEventFlag(MessageFlag, randomMessageFlag()); +getEventFlag(MessageFlag, (EventFlag) => { + console.log("inject EventFlag", EventFlag); + const isContent = typeof chrome.runtime?.sendMessage === "function"; + const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; -const isContent = typeof chrome.runtime?.sendMessage === "function"; -const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; + const msg: Message = new CustomEventMessage(`${EventFlag}${scriptEnvTag}`, false); -const msg: Message = new CustomEventMessage(`${EventFlag}${scriptEnvTag}`, false); + // 加载logger组件 + const logger = new LoggerCore({ + writer: new MessageWriter(msg, "scripting/logger"), + consoleLevel: "none", // 只让日志在scripting环境中打印 + labels: { env: "inject", href: window.location.href }, + }); -// 加载logger组件 -const logger = new LoggerCore({ - writer: new MessageWriter(msg, "scripting/logger"), - consoleLevel: "none", // 只让日志在scripting环境中打印 - labels: { env: "inject", href: window.location.href }, -}); - -logger.logger().debug("inject start"); + logger.logger().debug("inject start"); -const server = new Server("inject", msg); -const scriptExecutor = new ScriptExecutor(msg); -const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, MessageFlag); -runtime.init(); + const server = new Server("inject", msg); + const scriptExecutor = new ScriptExecutor(msg); + const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, MessageFlag); + runtime.init(); -// inject环境,直接判断白名单,注入对外接口 -runtime.externalMessage(); + // inject环境,直接判断白名单,注入对外接口 + runtime.externalMessage(); +}); diff --git a/src/scripting.ts b/src/scripting.ts index 12c898e33..058566e17 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -7,33 +7,33 @@ import { ScriptEnvTag } from "@Packages/message/consts"; import { Server } from "@Packages/message/server"; import ScriptingRuntime from "./app/service/content/scripting"; import { negotiateEventFlag } from "@Packages/message/common"; -import { randomMessageFlag } from "./pkg/utils/utils"; -const MessageFlag = process.env.SC_RANDOM_KEY; +const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; -const EventFlag = negotiateEventFlag(MessageFlag, randomMessageFlag()); +// 将初始化流程完成后,将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" }, + }); -// 建立与service_worker页面的连接 -const extMsgComm: Message = new ExtensionMessage(false); -// 初始化日志组件 -const logger = new LoggerCore({ - writer: new MessageWriter(extMsgComm, "serviceWorker/logger"), - labels: { env: "scripting" }, -}); - -logger.logger().debug("scripting start"); + logger.logger().debug("scripting start"); -const contentMsg = new CustomEventMessage(`${EventFlag}${ScriptEnvTag.content}`, true); -const injectMsg = new CustomEventMessage(`${EventFlag}${ScriptEnvTag.inject}`, true); + const contentMsg = new CustomEventMessage(`${eventFlag}${ScriptEnvTag.content}`, true); + const injectMsg = new CustomEventMessage(`${eventFlag}${ScriptEnvTag.inject}`, true); -const server = new Server("scripting", [contentMsg, injectMsg]); + const server = new Server("scripting", [contentMsg, injectMsg]); -// 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(); + // 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(); +}); From 59e0e677bca8bb78b4a37cb80002b1cc7bdd5d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 29 Jan 2026 18:10:15 +0800 Subject: [PATCH 18/37] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=8F=82=E6=95=B0=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 ++-- vitest.config.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 315dba9b2..892b5ef0a 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "private": true, "scripts": { "preinstall": "pnpm dlx only-allow pnpm", - "test": "vitest --test-timeout=500 --no-coverage --isolate=false --reporter=verbose", - "test:ci": "vitest run --test-timeout=500 --no-coverage --isolate=false --reporter=default --reporter.summary=false", + "test": "vitest --test-timeout=500 --no-coverage --reporter=verbose", + "test:ci": "vitest run --test-timeout=500 --no-coverage --reporter=default --reporter.summary=false", "coverage": "vitest run --coverage", "coverage:ci": "vitest run --coverage --silent --reporter=default --reporter.default.summary=false", "typecheck": "tsc --noEmit", diff --git a/vitest.config.ts b/vitest.config.ts index 80250fbf1..4360b4007 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -30,5 +30,6 @@ export default defineConfig({ env: { VI_TESTING: "true", }, + isolate: false, }, }); From 0471ca05bac9b88b5191ffa68f4d65fdaa5e7f06 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 19/37] 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 dc2d52174..17fe9356f 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -3,7 +3,8 @@ import { randomMessageFlag } from "@App/pkg/utils/utils"; // 避免页面载入后改动全域物件导致消息传递失败 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 e81b213d0..3e67e5d11 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -24,50 +24,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(() => { @@ -662,10 +658,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 cf1b0539c7d675ddb4a2359573e8c197677fc527 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:13:20 +0900 Subject: [PATCH 20/37] =?UTF-8?q?Revert:=20=E8=B0=83=E6=95=B4=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95=E5=8F=82=E6=95=B0=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 892b5ef0a..315dba9b2 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "private": true, "scripts": { "preinstall": "pnpm dlx only-allow pnpm", - "test": "vitest --test-timeout=500 --no-coverage --reporter=verbose", - "test:ci": "vitest run --test-timeout=500 --no-coverage --reporter=default --reporter.summary=false", + "test": "vitest --test-timeout=500 --no-coverage --isolate=false --reporter=verbose", + "test:ci": "vitest run --test-timeout=500 --no-coverage --isolate=false --reporter=default --reporter.summary=false", "coverage": "vitest run --coverage", "coverage:ci": "vitest run --coverage --silent --reporter=default --reporter.default.summary=false", "typecheck": "tsc --noEmit", From 790ce770f8a096152d96c30bc6122ae6871cb94f Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:15:25 +0900 Subject: [PATCH 21/37] lint --- packages/message/custom_event_message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index 69088f1ce..67e01a7c9 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -43,7 +43,7 @@ export class CustomEventMessage implements Message { ) { this.receiveFlag = `${messageFlag}${isInbound ? DefinedFlags.inboundFlag : DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`; this.sendFlag = `${messageFlag}${isInbound ? DefinedFlags.outboundFlag : DefinedFlags.inboundFlag}${DefinedFlags.domEvent}`; - pageAddEventListener(this.receiveFlag, (event) => { + pageAddEventListener(this.receiveFlag, (event: Event) => { if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) { relatedTargetMap.set(event.movementX, event.relatedTarget!); } else if (event instanceof CustomEventClone) { From 9ab440aebb9264f57526a29c07102a1505f6497f Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:19:51 +0900 Subject: [PATCH 22/37] =?UTF-8?q?Revert:=20=E8=B0=83=E6=95=B4=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95=E5=8F=82=E6=95=B0=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vitest.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/vitest.config.ts b/vitest.config.ts index 4360b4007..80250fbf1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -30,6 +30,5 @@ export default defineConfig({ env: { VI_TESTING: "true", }, - isolate: false, }, }); From d49a82e64d21fa8ea60a5e8faf45f20c49553b61 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:23:27 +0900 Subject: [PATCH 23/37] =?UTF-8?q?=E8=AA=BF=E6=95=B4=E4=BB=A3=E7=A2=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/custom_event_message.ts | 2 +- src/content.ts | 2 +- src/inject.ts | 2 +- src/scripting.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index 67e01a7c9..8ee87d0df 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -45,7 +45,7 @@ export class CustomEventMessage implements Message { this.sendFlag = `${messageFlag}${isInbound ? DefinedFlags.outboundFlag : DefinedFlags.inboundFlag}${DefinedFlags.domEvent}`; pageAddEventListener(this.receiveFlag, (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)); } diff --git a/src/content.ts b/src/content.ts index 3ed61a187..4db04e0d2 100644 --- a/src/content.ts +++ b/src/content.ts @@ -8,7 +8,7 @@ import { getEventFlag } from "@Packages/message/common"; import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; -const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; +const MessageFlag = process.env.SC_RANDOM_KEY!; getEventFlag(MessageFlag, (EventFlag) => { console.log("content EventFlag", EventFlag); diff --git a/src/inject.ts b/src/inject.ts index 9e733d0d8..5db97a60f 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -8,7 +8,7 @@ import { getEventFlag } from "@Packages/message/common"; import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; -const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; +const MessageFlag = process.env.SC_RANDOM_KEY!; getEventFlag(MessageFlag, (EventFlag) => { console.log("inject EventFlag", EventFlag); diff --git a/src/scripting.ts b/src/scripting.ts index 058566e17..5f5ffeb48 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -8,7 +8,7 @@ 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 || "scriptcat-default-flag"; +const MessageFlag = process.env.SC_RANDOM_KEY!; // 将初始化流程完成后,将EventFlag通知到其他环境 negotiateEventFlag(MessageFlag, 2, (eventFlag) => { From 1250bb9e9cda62b6e5b1ebc9ddc40b9ecd64c2fc Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:14:32 +0900 Subject: [PATCH 24/37] =?UTF-8?q?=E5=8A=A0=E5=85=A5=20readyDeferred=20?= =?UTF-8?q?=E5=92=8C=20isReady?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/common.ts | 15 +++++ packages/message/custom_event_message.ts | 72 +++++++++++++++------- packages/message/server.test.ts | 77 ++++++++++++++---------- 3 files changed, 108 insertions(+), 56 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index 17fe9356f..9b34ba8be 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -72,3 +72,18 @@ export function getEventFlag(messageFlag: string, onReady: (eventFlag: string) = 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 8ee87d0df..666a9094d 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -9,7 +9,9 @@ import { pageDispatchCustomEvent, MouseEventClone, CustomEventClone, + createMouseEvent, } from "@Packages/message/common"; +import { deferred } from "@App/pkg/utils/utils"; // 避免页面载入后改动 Map.prototype 导致消息传递失败 const relatedTargetMap = new Map(); @@ -36,6 +38,8 @@ export class CustomEventMessage implements Message { // 关联dom目标 relatedTarget: Map = new Map(); + readyDeferred = deferred(); + isReady = false; constructor( messageFlag: string, @@ -43,13 +47,28 @@ export class CustomEventMessage implements Message { ) { this.receiveFlag = `${messageFlag}${isInbound ? DefinedFlags.inboundFlag : DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`; this.sendFlag = `${messageFlag}${isInbound ? DefinedFlags.outboundFlag : DefinedFlags.inboundFlag}${DefinedFlags.domEvent}`; + const setReady = () => { + this.readyDeferred.resolve(); + this.isReady = true; + }; pageAddEventListener(this.receiveFlag, (event: Event) => { - if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) { + if (event instanceof MouseEventClone && event.movementX === 0 && event.cancelable) { + event.preventDefault(); // 告知另一端这边已准备好 + 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)); } }); + const ev = createMouseEvent(this.sendFlag, { + movementX: 0, + cancelable: true, + }); + //@ts-ignore + if (process.env.VI_TESTING === "true") ev.movementX = 0; + // 如另一端已准备好,则 setReady() + if (pageDispatchEvent(ev) === false) setReady(); } messageHandle(data: WindowMessageBody, target: PostMessage) { @@ -93,36 +112,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.readyDeferred.promise.then(() => { + 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 (!this.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) => { - 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.readyDeferred.promise.then(() => { + 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); + // }); }); } @@ -130,6 +154,7 @@ export class CustomEventMessage implements Message { // 与content页的消息通讯实际是同步,此方法不需要经过background // 但是请注意中间不要有promise syncSendMessage(data: TMessage): TMessage { + if (!this.isReady) throw new Error("custom_event_message is not ready."); const messageId = uuidv4(); const body: WindowMessageBody = { messageId, @@ -149,11 +174,12 @@ export class CustomEventMessage implements Message { } sendRelatedTarget(target: EventTarget): number { + if (!this.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(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 3e67e5d11..49a70b5df 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -13,43 +13,44 @@ let client: CustomEventMessage; const nextTick = () => Promise.resolve().then(() => {}); const setupGlobal = () => { - const testFlag = uuidv4(); - // 创建 scripting 和 inject / content 之间的消息通道 - inboundMessage = new CustomEventMessage(testFlag, true); // scripting 端 - outboundMessage = new CustomEventMessage(testFlag, false); // inject / content 端 - - // 服务端使用 scripting 消息 - server = new Server("api", inboundMessage); - - // 客户端使用 inject / content 消息 - client = outboundMessage; + const testFlag = `${uuidv4()}::server.test`; //@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(() => { + const eventType = event.type; + if (eventType.includes(testFlag)) { + if (event instanceof MouseEvent && event.movementX === 0) { + event.preventDefault(); // 告知另一端这边已准备好 + // setReady(); // 两端已准备好,则 setReady() + console.log("Simulation: setReady();"); + return false; + } else if (event instanceof MouseEvent && event.movementX && event.relatedTarget) { + // relatedTargetMap.set(event.movementX, event.relatedTarget); + console.log("Simulation: relatedTargetMap.set(event.movementX, event.relatedTarget);"); + return true; + } + 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(() => { + if (event instanceof CustomEvent) { messageThis.messageHandle(event.detail, { postMessage: (data: any) => { // 响应 @@ -59,11 +60,21 @@ const setupGlobal = () => { }); }, }); - }); - } + } + }); } return true; }); + + // 创建 scripting 和 inject / content 之间的消息通道 + inboundMessage = new CustomEventMessage(testFlag, true); // scripting 端 + outboundMessage = new CustomEventMessage(testFlag, false); // inject / content 端 + + // 服务端使用 scripting 消息 + server = new Server("api", inboundMessage); + + // 客户端使用 inject / content 消息 + client = outboundMessage; }; beforeEach(() => { From cf9029b42149d7255a049cc474cd34bcb7e06931 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:19:33 +0900 Subject: [PATCH 25/37] fix --- packages/message/custom_event_message.ts | 4 ++-- packages/message/server.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index 666a9094d..b1f334c41 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -132,7 +132,7 @@ export class CustomEventMessage implements Message { sendMessage(data: TMessage): Promise { return new Promise((resolve: ((value: T) => void) | null) => { - // this.readyDeferred.promise.then(() => { + this.readyDeferred.promise.then(() => { const messageId = uuidv4(); const body: WindowMessageBody = { messageId, @@ -146,7 +146,7 @@ export class CustomEventMessage implements Message { resolve = null; // 设为 null 提醒JS引擎可以GC }); this.nativeSend(body); - // }); + }); }); } diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index 49a70b5df..d3a341efa 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -288,7 +288,7 @@ describe("Server", () => { expect(response1.code).toBe(0); expect(response2.code).toBe(0); - expect(mockHandler).toHaveBeenCalledTimes(2); + // expect(mockHandler).toHaveBeenCalledTimes(2); }); }); From e7c93ca951118d6fa1033d4972527e65fa04b8cb Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:44:43 +0900 Subject: [PATCH 26/37] fix unit test --- packages/message/server.test.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index d3a341efa..2e412da74 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -265,15 +265,21 @@ describe("Server", () => { }); it.concurrent("应该自动为 Group 名称添加斜杠", async () => { - const mockHandler = vi.fn().mockResolvedValue("auto slash response"); + let count = 0; // 测试不带斜杠的情况 const group1 = server.group("slash-group1"); - group1.on("slash-test", mockHandler); + group1.on("slash-test", async () => { + count += 100; + return "auto slash response"; + }); // 测试带斜杠的情况 const group2 = server.group("slash-group2/"); - group2.on("slash-test", mockHandler); + group2.on("slash-test", async () => { + count += 200; + return "auto slash response"; + }); // 两种方式都应该工作 const response1 = await client.sendMessage({ @@ -288,7 +294,7 @@ describe("Server", () => { expect(response1.code).toBe(0); expect(response2.code).toBe(0); - // expect(mockHandler).toHaveBeenCalledTimes(2); + expect(count).toBe(300); }); }); From 267a24e2b931c963d65796f000aab55799c56cac 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 22:10:21 +0800 Subject: [PATCH 27/37] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/common.ts | 3 +- packages/message/server.test.ts | 56 ++------------------------------- 2 files changed, 4 insertions(+), 55 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index 9b34ba8be..0013d1cbb 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -3,8 +3,9 @@ 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); diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index 2e412da74..357a58e4e 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -2,7 +2,6 @@ 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 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; @@ -15,57 +14,6 @@ const nextTick = () => Promise.resolve().then(() => {}); const setupGlobal = () => { const testFlag = `${uuidv4()}::server.test`; - //@ts-ignore - const window = simulatedEventTarget; - - // 模拟消息传递 - 从 inject 到 content - window.dispatchEvent.mockImplementation((event: Event) => { - const eventType = event.type; - if (eventType.includes(testFlag)) { - if (event instanceof MouseEvent && event.movementX === 0) { - event.preventDefault(); // 告知另一端这边已准备好 - // setReady(); // 两端已准备好,则 setReady() - console.log("Simulation: setReady();"); - return false; - } else if (event instanceof MouseEvent && event.movementX && event.relatedTarget) { - // relatedTargetMap.set(event.movementX, event.relatedTarget); - console.log("Simulation: relatedTargetMap.set(event.movementX, event.relatedTarget);"); - return true; - } - 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(() => { - if (event instanceof CustomEvent) { - messageThis.messageHandle(event.detail, { - postMessage: (data: any) => { - // 响应 - const responseEvent = new CustomEvent(targetEventType, { detail: data }); - messageThat.messageHandle(responseEvent.detail, { - postMessage: vi.fn(), - }); - }, - }); - } - }); - } - return true; - }); - // 创建 scripting 和 inject / content 之间的消息通道 inboundMessage = new CustomEventMessage(testFlag, true); // scripting 端 outboundMessage = new CustomEventMessage(testFlag, false); // inject / content 端 @@ -675,10 +623,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, }); From 1178864825aec11adb57af75b9c8665c42d4647e 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 22:10:59 +0800 Subject: [PATCH 28/37] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/common.ts | 1 - tests/vitest.setup.ts | 6 ------ 2 files changed, 7 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index 0013d1cbb..2cb7b0f94 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -4,7 +4,6 @@ import { randomMessageFlag } from "@App/pkg/utils/utils"; export const MouseEventClone = MouseEvent; export const CustomEventClone = CustomEvent; -//@ts-ignore const performanceClone = (process.env.VI_TESTING === "true" ? new EventTarget() : performance) as Performance; // 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败 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 443681718673b8ef306c35a29e81d1285a0c754e 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 22:27:04 +0800 Subject: [PATCH 29/37] =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/server.test.ts | 4 ++-- tests/vitest.setup.ts | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index d42af37fb..357a58e4e 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -623,10 +623,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/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 d31baf32e117f74a395a473115a9c78b8a6545f5 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 31 Jan 2026 06:52:03 +0900 Subject: [PATCH 30/37] =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/server.test.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index 357a58e4e..f59b00abb 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -213,21 +213,15 @@ describe("Server", () => { }); it.concurrent("应该自动为 Group 名称添加斜杠", async () => { - let count = 0; + const mockHandler = vi.fn().mockResolvedValue("nested response"); // 测试不带斜杠的情况 const group1 = server.group("slash-group1"); - group1.on("slash-test", async () => { - count += 100; - return "auto slash response"; - }); + group1.on("slash-test", mockHandler); // 测试带斜杠的情况 const group2 = server.group("slash-group2/"); - group2.on("slash-test", async () => { - count += 200; - return "auto slash response"; - }); + group2.on("slash-test", mockHandler); // 两种方式都应该工作 const response1 = await client.sendMessage({ @@ -242,7 +236,7 @@ describe("Server", () => { expect(response1.code).toBe(0); expect(response2.code).toBe(0); - expect(count).toBe(300); + expect(mockHandler).toHaveBeenCalledTimes(2); }); }); From c11d6e7e4c3757b7802a0a1fe53981710a26f517 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 31 Jan 2026 07:03:29 +0900 Subject: [PATCH 31/37] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=A4=A7=E5=B0=8F?= =?UTF-8?q?=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/common.ts | 28 +++++++++++----------- src/app/service/content/script_executor.ts | 2 +- src/content.ts | 10 ++++---- src/inject.ts | 10 ++++---- src/scripting.ts | 4 ++-- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/message/common.ts b/packages/message/common.ts index 2cb7b0f94..bffdd6a40 100644 --- a/packages/message/common.ts +++ b/packages/message/common.ts @@ -24,49 +24,49 @@ 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/scripting 发来的请求 eventFlag 的消息 let ready = 0; - const EventFlagRequestHandler: EventListenerOrEventListenerObject = (ev) => { + const fnEventFlagRequestHandler: EventListener = (ev: Event) => { if (!(ev instanceof CustomEvent)) return; switch (ev.detail?.action) { case "receivedEventFlag": - // 对方已收到 EventFlag + // 对方已收到 eventFlag ready += 1; if (ready >= readyCount) { // 已收到两个环境的请求,移除监听 - pageRemoveEventListener(messageFlag, EventFlagRequestHandler); + pageRemoveEventListener(messageFlag, fnEventFlagRequestHandler); } break; case "requestEventFlag": // 广播通信 flag 给 inject/scripting - pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", EventFlag: eventFlag }); + pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag }); break; } }; - pageAddEventListener(messageFlag, EventFlagRequestHandler); + pageAddEventListener(messageFlag, fnEventFlagRequestHandler); // 广播通信 flag 给 inject/scripting - pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", EventFlag: eventFlag }); + pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag }); } -// 获取协商后的 EventFlag +// 获取协商后的 eventFlag export function getEventFlag(messageFlag: string, onReady: (eventFlag: string) => void) { let eventFlag = ""; - const EventFlagListener: EventListenerOrEventListenerObject = (ev) => { + const fnEventFlagListener: EventListener = (ev: Event) => { if (!(ev instanceof CustomEvent)) return; if (ev.detail?.action != "broadcastEventFlag") return; - eventFlag = ev.detail.EventFlag; - pageRemoveEventListener(messageFlag, EventFlagListener); - // 告知对方已收到 EventFlag + eventFlag = ev.detail.eventFlag; + pageRemoveEventListener(messageFlag, fnEventFlagListener); + // 告知对方已收到 eventFlag pageDispatchCustomEvent(messageFlag, { action: "receivedEventFlag" }); onReady(eventFlag); }; - pageAddEventListener(messageFlag, EventFlagListener); + pageAddEventListener(messageFlag, fnEventFlagListener); - // 基于同步机制,判断是否已经收到 EventFlag + // 基于同步机制,判断是否已经收到 eventFlag // 如果没有收到,则主动请求一次 if (!eventFlag) { pageDispatchCustomEvent(messageFlag, { action: "requestEventFlag" }); diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index 3b878a8fb..68759420b 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -92,7 +92,7 @@ export class ScriptExecutor { const envLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.envLoadComplete}`; // 监听 脚本加载 // 适用于此「通知环境加载完成」代码执行后的脚本加载 - const scriptLoadCompleteHandler: EventListener = (ev) => { + const scriptLoadCompleteHandler: EventListener = (ev: Event) => { const detail = (ev as CustomEvent).detail as { scriptFlag: string; scriptInfo: ScriptLoadInfo; diff --git a/src/content.ts b/src/content.ts index 4db04e0d2..9a6755b47 100644 --- a/src/content.ts +++ b/src/content.ts @@ -8,14 +8,14 @@ import { getEventFlag } from "@Packages/message/common"; import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; -const MessageFlag = process.env.SC_RANDOM_KEY!; +const messageFlag = process.env.SC_RANDOM_KEY!; -getEventFlag(MessageFlag, (EventFlag) => { - console.log("content EventFlag", EventFlag); +getEventFlag(messageFlag, (eventFlag: string) => { + console.log("content eventFlag", eventFlag); const isContent = typeof chrome.runtime?.sendMessage === "function"; const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; - const msg: Message = new CustomEventMessage(`${EventFlag}${scriptEnvTag}`, false); + const msg: Message = new CustomEventMessage(`${eventFlag}${scriptEnvTag}`, false); // 初始化日志组件 const logger = new LoggerCore({ @@ -27,6 +27,6 @@ getEventFlag(MessageFlag, (EventFlag) => { const server = new Server("content", msg); const scriptExecutor = new ScriptExecutor(msg); - const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, MessageFlag); + const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, messageFlag); runtime.init(); }); diff --git a/src/inject.ts b/src/inject.ts index 5db97a60f..d6df07336 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -8,14 +8,14 @@ import { getEventFlag } from "@Packages/message/common"; import { ScriptRuntime } from "./app/service/content/script_runtime"; import { ScriptEnvTag } from "@Packages/message/consts"; -const MessageFlag = process.env.SC_RANDOM_KEY!; +const messageFlag = process.env.SC_RANDOM_KEY!; -getEventFlag(MessageFlag, (EventFlag) => { - console.log("inject EventFlag", EventFlag); +getEventFlag(messageFlag, (eventFlag: string) => { + console.log("inject eventFlag", eventFlag); const isContent = typeof chrome.runtime?.sendMessage === "function"; const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; - const msg: Message = new CustomEventMessage(`${EventFlag}${scriptEnvTag}`, false); + const msg: Message = new CustomEventMessage(`${eventFlag}${scriptEnvTag}`, false); // 加载logger组件 const logger = new LoggerCore({ @@ -28,7 +28,7 @@ getEventFlag(MessageFlag, (EventFlag) => { const server = new Server("inject", msg); const scriptExecutor = new ScriptExecutor(msg); - const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, MessageFlag); + const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, messageFlag); runtime.init(); // inject环境,直接判断白名单,注入对外接口 diff --git a/src/scripting.ts b/src/scripting.ts index 5f5ffeb48..9ff6d0bb1 100644 --- a/src/scripting.ts +++ b/src/scripting.ts @@ -8,10 +8,10 @@ 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!; +const messageFlag = process.env.SC_RANDOM_KEY!; // 将初始化流程完成后,将EventFlag通知到其他环境 -negotiateEventFlag(MessageFlag, 2, (eventFlag) => { +negotiateEventFlag(messageFlag, 2, (eventFlag) => { // 建立与service_worker页面的连接 const extMsgComm: Message = new ExtensionMessage(false); // 初始化日志组件 From 11cdd347098d094fd709e3039da3a0fa40456a48 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 31 Jan 2026 07:06:48 +0900 Subject: [PATCH 32/37] =?UTF-8?q?=E5=88=AA=E7=84=A1=E7=94=A8=E4=BB=A3?= =?UTF-8?q?=E7=A2=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/custom_event_message.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index b1f334c41..a366a090d 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -65,8 +65,6 @@ export class CustomEventMessage implements Message { movementX: 0, cancelable: true, }); - //@ts-ignore - if (process.env.VI_TESTING === "true") ev.movementX = 0; // 如另一端已准备好,则 setReady() if (pageDispatchEvent(ev) === false) setReady(); } From 812625e8bb5cab522130e28e1a667debb8a1a3b8 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 31 Jan 2026 07:44:28 +0900 Subject: [PATCH 33/37] =?UTF-8?q?=E6=8A=BD=E5=8F=96=E8=87=B3=E5=85=B1?= =?UTF-8?q?=E9=80=9A=20ReadyWrap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/custom_event_message.ts | 23 +++++++++-------------- src/pkg/utils/ready-wrap.ts | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 src/pkg/utils/ready-wrap.ts diff --git a/packages/message/custom_event_message.ts b/packages/message/custom_event_message.ts index a366a090d..5943bb632 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -11,7 +11,7 @@ import { CustomEventClone, createMouseEvent, } from "@Packages/message/common"; -import { deferred } from "@App/pkg/utils/utils"; +import { ReadyWrap } from "@App/pkg/utils/ready-wrap"; // 避免页面载入后改动 Map.prototype 导致消息传递失败 const relatedTargetMap = new Map(); @@ -38,8 +38,7 @@ export class CustomEventMessage implements Message { // 关联dom目标 relatedTarget: Map = new Map(); - readyDeferred = deferred(); - isReady = false; + readyWrap: ReadyWrap = new ReadyWrap(); constructor( messageFlag: string, @@ -47,14 +46,10 @@ export class CustomEventMessage implements Message { ) { this.receiveFlag = `${messageFlag}${isInbound ? DefinedFlags.inboundFlag : DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`; this.sendFlag = `${messageFlag}${isInbound ? DefinedFlags.outboundFlag : DefinedFlags.inboundFlag}${DefinedFlags.domEvent}`; - const setReady = () => { - this.readyDeferred.resolve(); - this.isReady = true; - }; pageAddEventListener(this.receiveFlag, (event: Event) => { if (event instanceof MouseEventClone && event.movementX === 0 && event.cancelable) { event.preventDefault(); // 告知另一端这边已准备好 - setReady(); // 两端已准备好,则 setReady() + this.readyWrap.setReady(); // 两端已准备好,则 setReady() } else if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) { relatedTargetMap.set(event.movementX, event.relatedTarget); } else if (event instanceof CustomEventClone) { @@ -66,7 +61,7 @@ export class CustomEventMessage implements Message { cancelable: true, }); // 如另一端已准备好,则 setReady() - if (pageDispatchEvent(ev) === false) setReady(); + if (pageDispatchEvent(ev) === false) this.readyWrap.setReady(); } messageHandle(data: WindowMessageBody, target: PostMessage) { @@ -110,7 +105,7 @@ export class CustomEventMessage implements Message { connect(data: TMessage): Promise { return new Promise((resolve) => { - this.readyDeferred.promise.then(() => { + this.readyWrap.onReady(() => { const body: WindowMessageBody = { messageId: uuidv4(), type: "connect", @@ -124,13 +119,13 @@ export class CustomEventMessage implements Message { } nativeSend(detail: any) { - if (!this.isReady) throw new Error("custom_event_message is not ready."); + 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.readyDeferred.promise.then(() => { + this.readyWrap.onReady(() => { const messageId = uuidv4(); const body: WindowMessageBody = { messageId, @@ -152,7 +147,7 @@ export class CustomEventMessage implements Message { // 与content页的消息通讯实际是同步,此方法不需要经过background // 但是请注意中间不要有promise syncSendMessage(data: TMessage): TMessage { - if (!this.isReady) throw new Error("custom_event_message is not ready."); + if (!this.readyWrap.isReady) throw new Error("custom_event_message is not ready."); const messageId = uuidv4(); const body: WindowMessageBody = { messageId, @@ -172,7 +167,7 @@ export class CustomEventMessage implements Message { } sendRelatedTarget(target: EventTarget): number { - if (!this.isReady) throw new Error("custom_event_message is not ready."); + 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); diff --git a/src/pkg/utils/ready-wrap.ts b/src/pkg/utils/ready-wrap.ts new file mode 100644 index 000000000..91862bb32 --- /dev/null +++ b/src/pkg/utils/ready-wrap.ts @@ -0,0 +1,14 @@ +export class ReadyWrap { + public isReady: boolean = false; + private resolve: ((value: void | PromiseLike) => void) | undefined; + private readonly promise: Promise = new Promise((resolve) => { + this.resolve = resolve; + }); + onReady(fn: () => any) { + this.isReady ? fn() : this.promise.then(fn); + } + setReady() { + this.resolve!(); + this.isReady = true; + } +} From 417d80aedafc4a343b08c0ff5f3d04d93f3ac93f Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 31 Jan 2026 07:48:50 +0900 Subject: [PATCH 34/37] =?UTF-8?q?ReadyWrap=20=E9=87=8B=E6=94=BE=E5=B7=B2?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20resolve,=20promise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pkg/utils/ready-wrap.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pkg/utils/ready-wrap.ts b/src/pkg/utils/ready-wrap.ts index 91862bb32..ef2edb36d 100644 --- a/src/pkg/utils/ready-wrap.ts +++ b/src/pkg/utils/ready-wrap.ts @@ -1,14 +1,16 @@ export class ReadyWrap { public isReady: boolean = false; - private resolve: ((value: void | PromiseLike) => void) | undefined; - private readonly promise: Promise = new Promise((resolve) => { + 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); + this.isReady ? fn() : this.promise!.then(fn); } setReady() { - this.resolve!(); + this.resolve?.(); this.isReady = true; + this.resolve = null; + this.promise = null; } } From 34241e5e6dfa98e72c10992b26e277db57ecf0b2 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 09:28:32 +0800 Subject: [PATCH 35/37] =?UTF-8?q?=E5=88=A0=E9=99=A4debug=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E5=92=8C=E8=B0=83=E6=95=B4=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/tests/early_inject_content_test.js | 146 ++++++++++++--------- example/tests/early_test.js | 144 +++++++++++--------- src/content.ts | 2 +- src/inject.ts | 1 - 4 files changed, 165 insertions(+), 128 deletions(-) diff --git a/example/tests/early_inject_content_test.js b/example/tests/early_inject_content_test.js index b3f9474cf..5420f216f 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,32 +153,6 @@ 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;"); diff --git a/example/tests/early_test.js b/example/tests/early_test.js index 2e835e3e5..7f44b44e1 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,32 +179,6 @@ } }); - // ============ 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;"); diff --git a/src/content.ts b/src/content.ts index 9a6755b47..acecfda81 100644 --- a/src/content.ts +++ b/src/content.ts @@ -11,7 +11,6 @@ import { ScriptEnvTag } from "@Packages/message/consts"; const messageFlag = process.env.SC_RANDOM_KEY!; getEventFlag(messageFlag, (eventFlag: string) => { - console.log("content eventFlag", eventFlag); const isContent = typeof chrome.runtime?.sendMessage === "function"; const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; @@ -20,6 +19,7 @@ getEventFlag(messageFlag, (eventFlag: string) => { // 初始化日志组件 const logger = new LoggerCore({ writer: new MessageWriter(msg, "scripting/logger"), + consoleLevel: "none", // 只让日志在scripting环境中打印 labels: { env: "content", href: window.location.href }, }); diff --git a/src/inject.ts b/src/inject.ts index d6df07336..8bfefe30b 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -11,7 +11,6 @@ import { ScriptEnvTag } from "@Packages/message/consts"; const messageFlag = process.env.SC_RANDOM_KEY!; getEventFlag(messageFlag, (eventFlag: string) => { - console.log("inject eventFlag", eventFlag); const isContent = typeof chrome.runtime?.sendMessage === "function"; const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; From f0ec0db2140406933e4563f7f2cbaa0e8a455d7c 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 09:41:00 +0800 Subject: [PATCH 36/37] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E7=AD=89=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/content.ts | 2 +- src/inject.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content.ts b/src/content.ts index acecfda81..130de1d13 100644 --- a/src/content.ts +++ b/src/content.ts @@ -19,7 +19,7 @@ getEventFlag(messageFlag, (eventFlag: string) => { // 初始化日志组件 const logger = new LoggerCore({ writer: new MessageWriter(msg, "scripting/logger"), - consoleLevel: "none", // 只让日志在scripting环境中打印 + consoleLevel: process.env.NODE_ENV === "development" ? "debug" : "none", // 只让日志在scripting环境中打印 labels: { env: "content", href: window.location.href }, }); diff --git a/src/inject.ts b/src/inject.ts index 8bfefe30b..3350b866a 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -19,7 +19,7 @@ getEventFlag(messageFlag, (eventFlag: string) => { // 加载logger组件 const logger = new LoggerCore({ writer: new MessageWriter(msg, "scripting/logger"), - consoleLevel: "none", // 只让日志在scripting环境中打印 + consoleLevel: process.env.NODE_ENV === "development" ? "debug" : "none", // 只让日志在scripting环境中打印 labels: { env: "inject", href: window.location.href }, }); From 2a6a0494ff686d5b4218d1945246cdd2809d22e8 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 09:46:17 +0800 Subject: [PATCH 37/37] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=84=9A=E6=9C=ACGM=20log=E4=BD=BF=E7=94=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/tests/early_inject_content_test.js | 2 +- example/tests/early_test.js | 2 +- example/tests/inject_content_test.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/tests/early_inject_content_test.js b/example/tests/early_inject_content_test.js index 5420f216f..1abfa9803 100644 --- a/example/tests/early_inject_content_test.js +++ b/example/tests/early_inject_content_test.js @@ -157,7 +157,7 @@ testSync("GM_addStyle", () => { 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 7f44b44e1..9ff6581d7 100644 --- a/example/tests/early_test.js +++ b/example/tests/early_test.js @@ -183,7 +183,7 @@ testSync("GM_addStyle", () => { 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应该能正常输出"); });