diff --git a/example/tests/early_inject_content_test.js b/example/tests/early_inject_content_test.js index b3f9474cf..1abfa9803 100644 --- a/example/tests/early_inject_content_test.js +++ b/example/tests/early_inject_content_test.js @@ -18,47 +18,93 @@ // @run-at document-start // ==/UserScript== -(async function () { - "use strict"; - - console.log("%c=== Content环境 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;"); - - let testResults = { - passed: 0, - failed: 0, - total: 0, - }; - - // 测试辅助函数(支持同步和异步) - async function test(name, fn) { - testResults.total++; - try { - await fn(); - testResults.passed++; - console.log(`%c✓ ${name}`, "color: green;"); - return true; - } catch (error) { - testResults.failed++; - console.error(`%c✗ ${name}`, "color: red;", error); - return false; - } +// 测试辅助函数(支持同步和异步) +async function test(name, fn) { + testResults.total++; + try { + await fn(); + testResults.passed++; + console.log(`%c✓ ${name}`, "color: green;"); + return true; + } catch (error) { + testResults.failed++; + console.error(`%c✗ ${name}`, "color: red;", error); + return false; } - - // assert(expected, actual, message) - 比较两个值是否相等 - function assert(expected, actual, message) { - if (expected !== actual) { - const valueInfo = `期望 ${JSON.stringify(expected)}, 实际 ${JSON.stringify(actual)}`; - const error = message ? `${message} - ${valueInfo}` : `断言失败: ${valueInfo}`; - throw new Error(error); - } +} + +function testSync(name, fn) { + testResults.total++; + try { + fn(); + testResults.passed++; + console.log(`%c✓ ${name}`, "color: green;"); + return true; + } catch (error) { + testResults.failed++; + console.error(`%c✗ ${name}`, "color: red;", error); + return false; } +} + +// assert(expected, actual, message) - 比较两个值是否相等 +function assert(expected, actual, message) { + if (expected !== actual) { + const valueInfo = `期望 ${JSON.stringify(expected)}, 实际 ${JSON.stringify(actual)}`; + const error = message ? `${message} - ${valueInfo}` : `断言失败: ${valueInfo}`; + throw new Error(error); + } +} - // assertTrue(condition, message) - 断言条件为真 - function assertTrue(condition, message) { - if (!condition) { - throw new Error(message || "断言失败: 期望条件为真"); - } +// assertTrue(condition, message) - 断言条件为真 +function assertTrue(condition, message) { + if (!condition) { + throw new Error(message || "断言失败: 期望条件为真"); } +} + +console.log("%c=== Content环境 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;"); + +let testResults = { + passed: 0, + failed: 0, + total: 0, +}; + +// 同步测试 + +// ============ GM_addElement/GM_addStyle 测试 ============ +console.log("\n%c--- DOM操作 API 测试 ---", "color: orange; font-weight: bold;"); + +testSync("GM_addElement", () => { + const element = GM_addElement("div", { + textContent: "GM_addElement测试元素", + style: "display:none;", + id: "gm-test-element", + }); + assertTrue(element !== null && element !== undefined, "GM_addElement应该返回元素"); + assert("gm-test-element", element.id, "元素ID应该正确"); + assert("DIV", element.tagName, "元素标签应该是DIV"); + console.log("返回元素:", element); + // 清理测试元素 + element.parentNode.removeChild(element); +}); + +testSync("GM_addStyle", () => { + const styleElement = GM_addStyle(` + .gm-style-test { + color: #10b981 !important; + } + `); + assertTrue(styleElement !== null && styleElement !== undefined, "GM_addStyle应该返回样式元素"); + assertTrue(styleElement.tagName === "STYLE" || styleElement.sheet, "应该返回STYLE元素或样式对象"); + console.log("返回样式元素:", styleElement); + // 清理测试样式 + styleElement.parentNode.removeChild(styleElement); +}); + +(async function () { + "use strict"; // ============ 早期脚本环境检查 ============ console.log("\n%c--- 早期脚本环境检查 ---", "color: orange; font-weight: bold;"); @@ -107,37 +153,11 @@ console.log("脚本注入到:", node.tagName); }); - // ============ GM_addElement/GM_addStyle 测试 ============ - console.log("\n%c--- DOM操作 API 测试 ---", "color: orange; font-weight: bold;"); - - await test("GM_addElement", () => { - const element = GM_addElement("div", { - textContent: "GM_addElement测试元素", - style: "display:none;", - id: "gm-test-element", - }); - assertTrue(element !== null && element !== undefined, "GM_addElement应该返回元素"); - assert("gm-test-element", element.id, "元素ID应该正确"); - assert("DIV", element.tagName, "元素标签应该是DIV"); - console.log("返回元素:", element); - }); - - await test("GM_addStyle", () => { - const styleElement = GM_addStyle(` - .gm-style-test { - color: #10b981 !important; - } - `); - assertTrue(styleElement !== null && styleElement !== undefined, "GM_addStyle应该返回样式元素"); - assertTrue(styleElement.tagName === "STYLE" || styleElement.sheet, "应该返回STYLE元素或样式对象"); - console.log("返回样式元素:", styleElement); - }); - // ============ GM_log 测试 ============ console.log("\n%c--- GM_log 测试 ---", "color: orange; font-weight: bold;"); await test("GM_log", () => { - GM_log("测试日志输出", { type: "test", value: 123 }); + GM_log("测试日志输出", "info", { type: "test", value: 123 }); // GM_log本身不返回值,只要不抛出异常就算成功 assertTrue(true, "GM_log应该能正常输出"); }); diff --git a/example/tests/early_test.js b/example/tests/early_test.js index 2e835e3e5..9ff6581d7 100644 --- a/example/tests/early_test.js +++ b/example/tests/early_test.js @@ -17,47 +17,91 @@ // @run-at document-start // ==/UserScript== -(async function () { - "use strict"; - - console.log("%c=== 早期脚本 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;"); - - let testResults = { - passed: 0, - failed: 0, - total: 0, - }; - - // 测试辅助函数(支持同步和异步) - async function test(name, fn) { - testResults.total++; - try { - await fn(); - testResults.passed++; - console.log(`%c✓ ${name}`, "color: green;"); - return true; - } catch (error) { - testResults.failed++; - console.error(`%c✗ ${name}`, "color: red;", error); - return false; - } +// 测试辅助函数(支持同步和异步) +async function test(name, fn) { + testResults.total++; + try { + await fn(); + testResults.passed++; + console.log(`%c✓ ${name}`, "color: green;"); + return true; + } catch (error) { + testResults.failed++; + console.error(`%c✗ ${name}`, "color: red;", error); + return false; } - - // assert(expected, actual, message) - 比较两个值是否相等 - function assert(expected, actual, message) { - if (expected !== actual) { - const valueInfo = `期望 ${JSON.stringify(expected)}, 实际 ${JSON.stringify(actual)}`; - const error = message ? `${message} - ${valueInfo}` : `断言失败: ${valueInfo}`; - throw new Error(error); - } +} + +function testSync(name, fn) { + testResults.total++; + try { + fn(); + testResults.passed++; + console.log(`%c✓ ${name}`, "color: green;"); + return true; + } catch (error) { + testResults.failed++; + console.error(`%c✗ ${name}`, "color: red;", error); + return false; } +} + +// assert(expected, actual, message) - 比较两个值是否相等 +function assert(expected, actual, message) { + if (expected !== actual) { + const valueInfo = `期望 ${JSON.stringify(expected)}, 实际 ${JSON.stringify(actual)}`; + const error = message ? `${message} - ${valueInfo}` : `断言失败: ${valueInfo}`; + throw new Error(error); + } +} - // assertTrue(condition, message) - 断言条件为真 - function assertTrue(condition, message) { - if (!condition) { - throw new Error(message || "断言失败: 期望条件为真"); - } +// assertTrue(condition, message) - 断言条件为真 +function assertTrue(condition, message) { + if (!condition) { + throw new Error(message || "断言失败: 期望条件为真"); } +} + +console.log("%c=== 早期脚本 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;"); + +let testResults = { + passed: 0, + failed: 0, + total: 0, +}; + +// ============ GM_addElement/GM_addStyle 测试 ============ +console.log("\n%c--- DOM操作 API 测试 ---", "color: orange; font-weight: bold;"); + +testSync("GM_addElement", () => { + const element = GM_addElement("div", { + textContent: "GM_addElement测试元素", + style: "display:none;", + id: "gm-test-element", + }); + assertTrue(element !== null && element !== undefined, "GM_addElement应该返回元素"); + assert("gm-test-element", element.id, "元素ID应该正确"); + assert("DIV", element.tagName, "元素标签应该是DIV"); + console.log("返回元素:", element); + // 清理测试元素 + element.parentNode.removeChild(element); +}); + +testSync("GM_addStyle", () => { + const styleElement = GM_addStyle(` + .gm-style-test { + color: #10b981 !important; + } + `); + assertTrue(styleElement !== null && styleElement !== undefined, "GM_addStyle应该返回样式元素"); + assertTrue(styleElement.tagName === "STYLE" || styleElement.sheet, "应该返回STYLE元素或样式对象"); + console.log("返回样式元素:", styleElement); + // 清理测试样式 + styleElement.parentNode.removeChild(styleElement); +}); + +(async function () { + "use strict"; // ============ 早期脚本环境检查 ============ console.log("\n%c--- 早期脚本环境检查 ---", "color: orange; font-weight: bold;"); @@ -135,37 +179,11 @@ } }); - // ============ GM_addElement/GM_addStyle 测试 ============ - console.log("\n%c--- DOM操作 API 测试 ---", "color: orange; font-weight: bold;"); - - await test("GM_addElement", () => { - const element = GM_addElement("div", { - textContent: "GM_addElement测试元素", - style: "display:none;", - id: "gm-test-element", - }); - assertTrue(element !== null && element !== undefined, "GM_addElement应该返回元素"); - assert("gm-test-element", element.id, "元素ID应该正确"); - assert("DIV", element.tagName, "元素标签应该是DIV"); - console.log("返回元素:", element); - }); - - await test("GM_addStyle", () => { - const styleElement = GM_addStyle(` - .gm-style-test { - color: #10b981 !important; - } - `); - assertTrue(styleElement !== null && styleElement !== undefined, "GM_addStyle应该返回样式元素"); - assertTrue(styleElement.tagName === "STYLE" || styleElement.sheet, "应该返回STYLE元素或样式对象"); - console.log("返回样式元素:", styleElement); - }); - // ============ GM_log 测试 ============ console.log("\n%c--- GM_log 测试 ---", "color: orange; font-weight: bold;"); await test("GM_log", () => { - GM_log("测试日志输出", { type: "test", value: 123 }); + GM_log("测试日志输出", "info", { type: "test", value: 123 }); // GM_log本身不返回值,只要不抛出异常就算成功 assertTrue(true, "GM_log应该能正常输出"); }); diff --git a/example/tests/inject_content_test.js b/example/tests/inject_content_test.js index c2a2ca981..b16c9d25e 100644 --- a/example/tests/inject_content_test.js +++ b/example/tests/inject_content_test.js @@ -99,7 +99,7 @@ console.log("\n%c--- GM_log 测试 ---", "color: orange; font-weight: bold;"); await test("GM_log", () => { - GM_log("测试日志输出", { type: "test", value: 123 }); + GM_log("测试日志输出", "info", { type: "test", value: 123 }); // GM_log本身不返回值,只要不抛出异常就算成功 assertTrue(true, "GM_log应该能正常输出"); }); diff --git a/packages/message/common.ts b/packages/message/common.ts new file mode 100644 index 000000000..799a8526d --- /dev/null +++ b/packages/message/common.ts @@ -0,0 +1,84 @@ +import { randomMessageFlag } from "@App/pkg/utils/utils"; + +// 避免页面载入后改动全域物件导致消息传递失败 +export const MouseEventClone = MouseEvent; +export const CustomEventClone = CustomEvent; + +const performanceClone = (process.env.VI_TESTING === "true" ? new EventTarget() : performance) as Performance; + +// 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败 +export const pageDispatchEvent = performanceClone.dispatchEvent.bind(performanceClone); +export const pageAddEventListener = performanceClone.addEventListener.bind(performanceClone); +export const pageRemoveEventListener = performanceClone.removeEventListener.bind(performanceClone); +const detailClone = typeof cloneInto === "function" ? cloneInto : null; +export const pageDispatchCustomEvent = (eventType: string, detail: any) => { + if (detailClone && detail) detail = detailClone(detail, performanceClone); + const ev = new CustomEventClone(eventType, { + detail, + cancelable: true, + }); + return pageDispatchEvent(ev); +}; + +// flag协商 +export function negotiateEventFlag(messageFlag: string, readyCount: number, onInit: (eventFlag: string) => void): void { + const eventFlag = randomMessageFlag(); + onInit(eventFlag); + // 监听 inject/content 发来的请求 eventFlag 的消息 + let ready = 0; + const fnEventFlagRequestHandler: EventListener = (ev: Event) => { + if (!(ev instanceof CustomEvent)) return; + + switch (ev.detail?.action) { + case "receivedEventFlag": + // 对方已收到 eventFlag + ready += 1; + if (ready >= readyCount) { + // 已收到两个环境的请求,移除监听 + pageRemoveEventListener(messageFlag, fnEventFlagRequestHandler); + } + break; + case "requestEventFlag": + // 广播通信 flag 给 inject/content + pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag }); + break; + } + }; + + // 设置事件,然后广播通信 flag 给 inject/content + pageAddEventListener(messageFlag, fnEventFlagRequestHandler); + pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag }); +} + +// 获取协商后的 eventFlag +export function getEventFlag(messageFlag: string, onReady: (eventFlag: string) => void) { + let eventFlag = ""; + const fnEventFlagListener: EventListener = (ev: Event) => { + if (!(ev instanceof CustomEvent)) return; + if (ev.detail?.action != "broadcastEventFlag") return; + eventFlag = ev.detail.eventFlag; + pageRemoveEventListener(messageFlag, fnEventFlagListener); + // 告知对方已收到 eventFlag + pageDispatchCustomEvent(messageFlag, { action: "receivedEventFlag" }); + onReady(eventFlag); + }; + + // 设置事件,然后对 scripting 请求 flag + pageAddEventListener(messageFlag, fnEventFlagListener); + 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/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 b3e8cf952..5943bb632 100644 --- a/packages/message/custom_event_message.ts +++ b/packages/message/custom_event_message.ts @@ -1,17 +1,17 @@ import type { Message, MessageConnect, RuntimeMessageSender, TMessage } from "./types"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import { type PostMessage, type WindowMessageBody, WindowMessageConnect } from "./window_message"; -import LoggerCore from "@App/app/logger/core"; import EventEmitter from "eventemitter3"; import { DefinedFlags } from "@App/app/service/service_worker/runtime.consts"; - -// 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败 -const pageDispatchEvent = performance.dispatchEvent.bind(performance); -const pageAddEventListener = performance.addEventListener.bind(performance); - -// 避免页面载入后改动全域物件导致消息传递失败 -const MouseEventClone = MouseEvent; -const CustomEventClone = CustomEvent; +import { + pageDispatchEvent, + pageAddEventListener, + pageDispatchCustomEvent, + MouseEventClone, + CustomEventClone, + createMouseEvent, +} from "@Packages/message/common"; +import { ReadyWrap } from "@App/pkg/utils/ready-wrap"; // 避免页面载入后改动 Map.prototype 导致消息传递失败 const relatedTargetMap = new Map(); @@ -38,20 +38,30 @@ export class CustomEventMessage implements Message { // 关联dom目标 relatedTarget: Map = new Map(); + readyWrap: ReadyWrap = new ReadyWrap(); constructor( messageFlag: string, - protected readonly isContent: boolean + protected readonly isInbound: boolean ) { - this.receiveFlag = `evt${messageFlag}${isContent ? DefinedFlags.contentFlag : DefinedFlags.injectFlag}${DefinedFlags.domEvent}`; - this.sendFlag = `evt${messageFlag}${isContent ? DefinedFlags.injectFlag : DefinedFlags.contentFlag}${DefinedFlags.domEvent}`; - pageAddEventListener(this.receiveFlag, (event) => { - if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) { - relatedTargetMap.set(event.movementX, event.relatedTarget!); + this.receiveFlag = `${messageFlag}${isInbound ? DefinedFlags.inboundFlag : DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`; + this.sendFlag = `${messageFlag}${isInbound ? DefinedFlags.outboundFlag : DefinedFlags.inboundFlag}${DefinedFlags.domEvent}`; + pageAddEventListener(this.receiveFlag, (event: Event) => { + if (event instanceof MouseEventClone && event.movementX === 0 && event.cancelable) { + event.preventDefault(); // 告知另一端这边已准备好 + this.readyWrap.setReady(); // 两端已准备好,则 setReady() + } else if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) { + relatedTargetMap.set(event.movementX, event.relatedTarget); } else if (event instanceof CustomEventClone) { this.messageHandle(event.detail, new CustomEventPostMessage(this)); } }); + const ev = createMouseEvent(this.sendFlag, { + movementX: 0, + cancelable: true, + }); + // 如另一端已准备好,则 setReady() + if (pageDispatchEvent(ev) === false) this.readyWrap.setReady(); } messageHandle(data: WindowMessageBody, target: PostMessage) { @@ -95,49 +105,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.readyWrap.onReady(() => { + const body: WindowMessageBody = { + messageId: uuidv4(), + type: "connect", + data, + }; + this.nativeSend(body); + // EventEmitter3 采用同步事件设计,callback会被马上执行而不像传统javascript架构以下一个macrotask 执行 + resolve(new WindowMessageConnect(body.messageId, this.EE, new CustomEventPostMessage(this))); + }); }); } nativeSend(detail: any) { - if (typeof cloneInto !== "undefined") { - try { - LoggerCore.logger().info("nativeSend"); - detail = cloneInto(detail, document.defaultView); - } catch (e) { - console.log(e); - LoggerCore.logger().info("error data"); - } - } - - const ev = new CustomEventClone(this.sendFlag, { - detail, - }); - pageDispatchEvent(ev); + if (!this.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) => { - 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.readyWrap.onReady(() => { + const messageId = uuidv4(); + const body: WindowMessageBody = { + messageId, + type: "sendMessage", + data, + }; + const eventId = `response:${messageId}`; + this.EE.addListener(eventId, (body: WindowMessageBody) => { + this.EE.removeAllListeners(eventId); + resolve!(body.data as T); + resolve = null; // 设为 null 提醒JS引擎可以GC + }); + this.nativeSend(body); }); - this.nativeSend(body); }); } @@ -145,6 +147,7 @@ export class CustomEventMessage implements Message { // 与content页的消息通讯实际是同步,此方法不需要经过background // 但是请注意中间不要有promise syncSendMessage(data: TMessage): TMessage { + if (!this.readyWrap.isReady) throw new Error("custom_event_message is not ready."); const messageId = uuidv4(); const body: WindowMessageBody = { messageId, @@ -164,11 +167,12 @@ export class CustomEventMessage implements Message { } sendRelatedTarget(target: EventTarget): number { + if (!this.readyWrap.isReady) throw new Error("custom_event_message is not ready."); // 特殊处理relatedTarget,返回id进行关联 // 先将relatedTarget转换成id发送过去 const id = (relateId = relateId === maxInteger ? 1 : relateId + 1); // 可以使用此种方式交互element - const ev = new MouseEventClone(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 5347ed823..f59b00abb 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -2,71 +2,27 @@ 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 contentMessage: CustomEventMessage; -let injectMessage: CustomEventMessage; +let inboundMessage: CustomEventMessage; +let outboundMessage: CustomEventMessage; let server: Server; let client: CustomEventMessage; const nextTick = () => Promise.resolve().then(() => {}); const setupGlobal = () => { - const flags = "-test.server"; - // 创建 content 和 inject 之间的消息通道 - contentMessage = new CustomEventMessage(flags, true); // content 端 - injectMessage = new CustomEventMessage(flags, false); // inject 端 - - // 服务端使用 content 消息 - server = new Server("api", contentMessage); - - // 客户端使用 inject 消息 - client = injectMessage; - - // 清理 DOM 事件监听器 - vi.stubGlobal("window", Object.create(window)); - vi.stubGlobal("addEventListener", vi.fn()); - - // 模拟消息传递 - 从 inject 到 content - vi.stubGlobal( - "dispatchEvent", - vi.fn().mockImplementation((event: Event) => { - if (event instanceof CustomEvent) { - const eventType = event.type; - if (eventType.includes("-test.server")) { - let targetEventType: string; - let messageThis: CustomEventMessage; - let messageThat: CustomEventMessage; - // 根据事件类型确定目标消息处理器 - if (eventType.includes(DefinedFlags.contentFlag)) { - // inject -> content - targetEventType = eventType.replace(DefinedFlags.contentFlag, DefinedFlags.injectFlag); - messageThis = contentMessage; - messageThat = injectMessage; - } else if (eventType.includes(DefinedFlags.injectFlag)) { - // content -> inject - targetEventType = eventType.replace(DefinedFlags.injectFlag, DefinedFlags.contentFlag); - messageThis = injectMessage; - messageThat = contentMessage; - } 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; - }) - ); + const testFlag = `${uuidv4()}::server.test`; + + // 创建 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(() => { @@ -257,7 +213,7 @@ describe("Server", () => { }); it.concurrent("应该自动为 Group 名称添加斜杠", async () => { - const mockHandler = vi.fn().mockResolvedValue("auto slash response"); + const mockHandler = vi.fn().mockResolvedValue("nested response"); // 测试不带斜杠的情况 const group1 = server.group("slash-group1"); @@ -642,7 +598,7 @@ describe("Server", () => { }); it("应该在 enableConnect 为 false 时不处理连接", async () => { - const serverWithoutConnect = new Server("api", contentMessage, false); + const serverWithoutConnect = new Server("api", inboundMessage, false); const mockHandler = vi.fn(); serverWithoutConnect.on("on-noconnect", mockHandler); diff --git a/packages/message/window_message.ts b/packages/message/window_message.ts index d650dedbf..baedbbe30 100644 --- a/packages/message/window_message.ts +++ b/packages/message/window_message.ts @@ -1,5 +1,5 @@ import type { Message, MessageConnect, MessageSend, RuntimeMessageSender, TMessage } from "./types"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import EventEmitter from "eventemitter3"; // 通过 window.postMessage/onmessage 实现通信 diff --git a/rspack.config.ts b/rspack.config.ts index ecc2955b5..a2c47f83a 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from "@rspack/cli"; import { rspack } from "@rspack/core"; import { readFileSync } from "fs"; import { NormalModule } from "@rspack/core"; +import { v4 as uuidv4 } from "uuid"; const pkg = JSON.parse(readFileSync("./package.json", "utf-8")); @@ -19,7 +20,15 @@ const dist = path.join(dirname, "dist"); const assets = path.join(src, "assets"); // 排除这些文件,不进行分离 -const chunkExcludeSet = new Set(["editor.worker", "ts.worker", "linter.worker", "service_worker", "content", "inject"]); +const chunkExcludeSet = new Set([ + "editor.worker", + "ts.worker", + "linter.worker", + "service_worker", + "content", + "inject", + "scripting", +]); export default defineConfig({ ...(isDev @@ -38,6 +47,7 @@ export default defineConfig({ offscreen: `${src}/offscreen.ts`, sandbox: `${src}/sandbox.ts`, content: `${src}/content.ts`, + scripting: `${src}/scripting.ts`, inject: `${src}/inject.ts`, popup: `${src}/pages/popup/main.tsx`, install: `${src}/pages/install/main.tsx`, @@ -111,6 +121,10 @@ export default defineConfig({ ], }, plugins: [ + new rspack.DefinePlugin({ + "process.env.VI_TESTING": "'false'", + "process.env.SC_RANDOM_KEY": `'${uuidv4()}'`, + }), new rspack.CopyRspackPlugin({ patterns: [ { diff --git a/src/app/service/content/content.ts b/src/app/service/content/content.ts deleted file mode 100644 index fefd3ff0e..000000000 --- a/src/app/service/content/content.ts +++ /dev/null @@ -1,149 +0,0 @@ -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/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 69924277a..ce4f4994c 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -1,5 +1,5 @@ import type { TScriptInfo } from "@App/app/repo/scripts"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import type { Message } from "@Packages/message/types"; import EventEmitter from "eventemitter3"; import { GMContextApiGet } from "./gm_api/gm_context"; diff --git a/src/app/service/content/exec_script.ts b/src/app/service/content/exec_script.ts index 9d54bf883..9ec3c701d 100644 --- a/src/app/service/content/exec_script.ts +++ b/src/app/service/content/exec_script.ts @@ -25,7 +25,7 @@ export default class ExecScript { constructor( scriptRes: TScriptInfo, - envPrefix: "content" | "offscreen", + envPrefix: "scripting" | "offscreen", message: Message, code: string | ScriptFunc, envInfo: GMInfoEnv, @@ -48,7 +48,7 @@ export default class ExecScript { if (grantSet.has("none")) { // 不注入任何GM api // ScriptCat行为:GM.info 和 GM_info 同时注入 - // 不改变Context情况下,以 named 传多於一个全域变量 + // 在不改变 Context 的情况下,以 named 传入多个全域变量 this.named = { GM: { info: GM_info }, GM_info }; } else { // 构建脚本GM上下文 diff --git a/src/app/service/content/gm_api/gm_api.test.ts b/src/app/service/content/gm_api/gm_api.test.ts index 28c46ee12..5bcb55227 100644 --- a/src/app/service/content/gm_api/gm_api.test.ts +++ b/src/app/service/content/gm_api/gm_api.test.ts @@ -5,7 +5,7 @@ import type { GMInfoEnv, ScriptFunc } from "../types"; import { compileScript, compileScriptCode } from "../utils"; import type { Message } from "@Packages/message/types"; import { encodeRValue } from "@App/pkg/utils/message_value"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; const nilFn: ScriptFunc = () => {}; const scriptRes = { @@ -264,7 +264,7 @@ describe.concurrent("GM_menu", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); @@ -278,7 +278,7 @@ describe.concurrent("GM_menu", () => { expect(mockSendMessage).toHaveBeenCalledWith( expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_registerMenuCommand", params: [ @@ -314,7 +314,7 @@ describe.concurrent("GM_menu", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = exec.exec(); // 验证 sendMessage 是否被调用 @@ -336,7 +336,7 @@ describe.concurrent("GM_menu", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); @@ -350,7 +350,7 @@ describe.concurrent("GM_menu", () => { expect(mockSendMessage).toHaveBeenCalledWith( expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_registerMenuCommand", params: [ @@ -399,7 +399,7 @@ describe.concurrent("GM_menu", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); expect(ret).toEqual({ id1: "abc", id2: "abc", id3: 1, id4: 2, id5: "3", id6: 3, id7: 3, id8: 4 }); @@ -423,7 +423,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -434,7 +434,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValue", params: [expect.any(String), "a", 123], @@ -448,7 +448,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValue", params: [expect.any(String), "a"], @@ -477,7 +477,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -494,7 +494,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValues", params: [ @@ -518,7 +518,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValues", params: [ @@ -552,7 +552,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -568,7 +568,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValues", params: [ @@ -587,7 +587,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValue", params: [ @@ -621,7 +621,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -638,7 +638,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValues", params: [ @@ -662,7 +662,7 @@ describe.concurrent("GM_value", () => { expect(mockSendMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ - action: "content/runtime/gmApi", + action: "scripting/runtime/gmApi", data: { api: "GM_setValues", params: [ @@ -697,7 +697,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); expect(mockSendMessage).toHaveBeenCalledTimes(1); @@ -731,7 +731,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); // remote = true const retPromise = exec.exec(); @@ -757,7 +757,7 @@ describe.concurrent("GM_value", () => { sendMessage: mockSendMessage, } as unknown as Message; // @ts-ignore - const exec = new ExecScript(script, "content", mockMessage, nilFn, envInfo); + const exec = new ExecScript(script, "scripting", mockMessage, nilFn, envInfo); exec.scriptFunc = compileScript(compileScriptCode(script)); const retPromise = exec.exec(); diff --git a/src/app/service/content/gm_api/gm_api.ts b/src/app/service/content/gm_api/gm_api.ts index b661d1655..b34ba1581 100644 --- a/src/app/service/content/gm_api/gm_api.ts +++ b/src/app/service/content/gm_api/gm_api.ts @@ -37,6 +37,9 @@ export interface GMRequestHandle { abort: () => void; } +// 判断当前是否运行在 USER_SCRIPT 环境 (content环境) +const isContent = typeof chrome.runtime?.sendMessage === "function"; + const integrity = {}; // 仅防止非法实例化 let valChangeCounterId = 0; @@ -478,7 +481,7 @@ export default class GMApi extends GM_Base { @GMContext.API() public async CAT_fetchDocument(url: string): Promise { - return urlToDocumentInContentPage(this, url); + return urlToDocumentInContentPage(this, url, isContent); } static _GM_cookie( @@ -682,6 +685,7 @@ export default class GMApi extends GM_Base { { textContent: css, }, + isContent, ], }, }); @@ -716,7 +720,7 @@ export default class GMApi extends GM_Base { data: { uuid: this.scriptRes.uuid, api: "GM_addElement", - params: [parentNodeId, tagName, attrs], + params: [parentNodeId, tagName, attrs, isContent], }, }); if (resp.code) { diff --git a/src/app/service/content/gm_api/gm_xhr.ts b/src/app/service/content/gm_api/gm_xhr.ts index c7e5a1ca1..5f42a048e 100644 --- a/src/app/service/content/gm_api/gm_xhr.ts +++ b/src/app/service/content/gm_api/gm_xhr.ts @@ -94,9 +94,9 @@ export const convObjectToURL = async (object: string | URL | Blob | File | undef return url; }; -export const urlToDocumentInContentPage = async (a: GMApi, url: string) => { +export const urlToDocumentInContentPage = async (a: GMApi, url: string, isContent: boolean) => { // url (e.g. blob url) -> XMLHttpRequest (CONTENT) -> Document (CONTENT) - const nodeId = await a.sendMessage("CAT_fetchDocument", [url]); + const nodeId = await a.sendMessage("CAT_fetchDocument", [url, isContent]); return (a.message).getAndDelRelatedTarget(nodeId) as Document; }; diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index 1a79c39b3..68759420b 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -6,7 +6,9 @@ import type { GMInfoEnv, ScriptFunc, ValueUpdateDataEncoded } from "./types"; import { addStyleSheet, definePropertyListener } from "./utils"; import type { ScriptLoadInfo, TScriptInfo } from "@App/app/repo/scripts"; import { DefinedFlags } from "../service_worker/runtime.consts"; +import { pageAddEventListener, pageDispatchEvent } from "@Packages/message/common"; import { isUrlExcluded } from "@App/pkg/utils/match"; +import type { ScriptEnvTag } from "@Packages/message/consts"; export type ExecScriptEntry = { scriptLoadInfo: TScriptInfo; @@ -84,14 +86,13 @@ export class ScriptExecutor { }); } - checkEarlyStartScript(env: "content" | "inject", messageFlag: string, envInfo: GMInfoEnv) { - const isContent = env === "content"; - const eventNamePrefix = `evt${messageFlag}${isContent ? DefinedFlags.contentFlag : DefinedFlags.injectFlag}`; + checkEarlyStartScript(scriptEnvTag: ScriptEnvTag, envInfo: GMInfoEnv) { + const eventNamePrefix = `evt${process.env.SC_RANDOM_KEY}.${scriptEnvTag}`; // 仅用于early-start初始化 const scriptLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.scriptLoadComplete}`; const envLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.envLoadComplete}`; // 监听 脚本加载 // 适用于此「通知环境加载完成」代码执行后的脚本加载 - performance.addEventListener(scriptLoadCompleteEvtName, (ev) => { + const scriptLoadCompleteHandler: EventListener = (ev: Event) => { const detail = (ev as CustomEvent).detail as { scriptFlag: string; scriptInfo: ScriptLoadInfo; @@ -116,11 +117,12 @@ export class ScriptExecutor { } this.execEarlyScript(scriptFlag, detail.scriptInfo, envInfo); } - }); + }; + pageAddEventListener(scriptLoadCompleteEvtName, scriptLoadCompleteHandler); // 通知 环境 加载完成 // 适用于此「通知环境加载完成」代码执行前的脚本加载 const ev = new CustomEvent(envLoadCompleteEvtName); - performance.dispatchEvent(ev); + pageDispatchEvent(ev); } execEarlyScript(flag: string, scriptInfo: TScriptInfo, envInfo: GMInfoEnv) { @@ -137,7 +139,7 @@ export class ScriptExecutor { execScriptEntry(scriptEntry: ExecScriptEntry) { const { scriptLoadInfo, scriptFunc, envInfo } = scriptEntry; - const exec = new ExecScript(scriptLoadInfo, "content", this.msg, scriptFunc, envInfo); + const exec = new ExecScript(scriptLoadInfo, "scripting", this.msg, scriptFunc, envInfo); this.execMap.set(scriptLoadInfo.uuid, exec); const metadata = scriptLoadInfo.metadata || {}; const resource = scriptLoadInfo.resource; diff --git a/src/app/service/content/inject.ts b/src/app/service/content/script_runtime.ts similarity index 79% rename from src/app/service/content/inject.ts rename to src/app/service/content/script_runtime.ts index a503529bb..93954a77f 100644 --- a/src/app/service/content/inject.ts +++ b/src/app/service/content/script_runtime.ts @@ -2,16 +2,19 @@ import { type Server } from "@Packages/message/server"; import type { Message } from "@Packages/message/types"; import { ExternalWhitelist } from "@App/app/const"; import { sendMessage } from "@Packages/message/client"; -import type { ScriptExecutor } from "./script_executor"; +import { 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 InjectRuntime { +export class ScriptRuntime { constructor( + private readonly scripEnvTag: ScriptEnvTag, private readonly server: Server, private readonly msg: Message, - private readonly scriptExecutor: ScriptExecutor + private readonly scriptExecutor: ScriptExecutor, + private readonly messageFlag: string ) {} init() { @@ -22,15 +25,18 @@ export class InjectRuntime { this.server.on("runtime/valueUpdate", (data: ValueUpdateDataEncoded) => { this.scriptExecutor.valueUpdate(data); }); - } - startScripts(injectScriptList: TScriptInfo[], envInfo: GMInfoEnv) { - this.scriptExecutor.startScripts(injectScriptList, envInfo); + this.server.on("pageLoad", (data: { scripts: TScriptInfo[]; envInfo: GMInfoEnv }) => { + // 监听事件 + this.startScripts(data.scripts, data.envInfo); + }); + + // 检查early-start的脚本 + this.scriptExecutor.checkEarlyStartScript(this.scripEnvTag, initEnvInfo); } - onInjectPageLoaded() { - // 注入允许外部调用 - this.externalMessage(); + startScripts(scripts: TScriptInfo[], envInfo: GMInfoEnv) { + this.scriptExecutor.startScripts(scripts, envInfo); } externalMessage() { diff --git a/src/app/service/content/scripting.ts b/src/app/service/content/scripting.ts new file mode 100644 index 000000000..17d1b0a84 --- /dev/null +++ b/src/app/service/content/scripting.ts @@ -0,0 +1,171 @@ +import { Client, sendMessage } from "@Packages/message/client"; +import { type CustomEventMessage } from "@Packages/message/custom_event_message"; +import { forwardMessage, type Server } from "@Packages/message/server"; +import type { MessageSend } from "@Packages/message/types"; +import { RuntimeClient } from "../service_worker/client"; +import { makeBlobURL } from "@App/pkg/utils/utils"; +import type { Logger } from "@App/app/repo/logger"; +import LoggerCore from "@App/app/logger/core"; +import type { ValueUpdateDataEncoded } from "./types"; + +// scripting页的处理 +export default class ScriptingRuntime { + constructor( + // 监听来自service_worker的消息 + private readonly extServer: Server, + // 监听来自inject的消息 + private readonly server: Server, + // 发送给扩展service_worker的通信接口 + private readonly senderToExt: MessageSend, + // 发送给 content的消息接口 + private readonly senderToContent: CustomEventMessage, + // 发送给inject的消息接口 + private readonly senderToInject: CustomEventMessage + ) {} + + // 广播消息给 content 和 inject + broadcastToPage(action: string, data?: any): Promise { + return Promise.all([ + sendMessage(this.senderToContent, "content/" + action, data), + sendMessage(this.senderToInject, "inject/" + action, data), + ]).then(() => undefined); + } + + init() { + this.extServer.on("runtime/emitEvent", (data) => { + // 转发给inject和content + return this.broadcastToPage("runtime/emitEvent", data); + }); + this.extServer.on("runtime/valueUpdate", (data) => { + // 转发给inject和content + return this.broadcastToPage("runtime/valueUpdate", data); + }); + this.server.on("logger", (data: Logger) => { + LoggerCore.logger().log(data.level, data.message, data.label); + }); + + // ================================ + // 来自 service_worker 的投递:storage 广播(类似 UDP) + // ================================ + + // 接收 service_worker 的 chrome.storage.local 值改变通知 (一对多广播) + // 类似 UDP 原理,service_worker 不会有任何「等待处理」 + // 由于 changes 会包括新旧值 (Chrome: JSON serialization, Firefox: Structured Clone) + // 因此需要注意资讯量不要过大导致 onChanged 的触发过慢 + chrome.storage.local.onChanged.addListener((changes) => { + if (changes["valueUpdateDelivery"]?.newValue) { + // 转发给 content 和 inject + this.broadcastToPage( + "runtime/valueUpdate", + changes["valueUpdateDelivery"]?.newValue.sendData as ValueUpdateDataEncoded + ); + } + }); + + forwardMessage("serviceWorker", "script/isInstalled", this.server, this.senderToExt); + forwardMessage( + "serviceWorker", + "runtime/gmApi", + this.server, + this.senderToExt, + (data: { api: string; params: any; uuid: string }) => { + // 拦截关注的 API,未命中则返回 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 ? this.senderToContent : this.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 ? this.senderToContent : this.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; + } + ); + } + + pageLoad() { + const client = new RuntimeClient(this.senderToExt); + // 向service_worker请求脚本列表及环境信息 + client.pageLoad().then((o) => { + if (!o.ok) return; + const { injectScriptList, contentScriptList, envInfo } = o; + + // 向页面 发送脚本列表及环境信息 + if (contentScriptList.length) { + const contentClient = new Client(this.senderToContent, "content"); + // 根据@inject-into content过滤脚本 + contentClient.do("pageLoad", { scripts: contentScriptList, envInfo }); + } + + if (injectScriptList.length) { + const injectClient = new Client(this.senderToInject, "inject"); + // 根据@inject-into content过滤脚本 + injectClient.do("pageLoad", { scripts: injectScriptList, envInfo }); + } + }); + } +} diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index 0c2c1c5c4..6fe01c634 100644 --- a/src/app/service/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -3,6 +3,7 @@ import type { ScriptFunc } from "./types"; import type { ScriptLoadInfo } from "../service_worker/types"; import { DefinedFlags } from "../service_worker/runtime.consts"; import { sourceMapTo } from "@App/pkg/utils/utils"; +import { ScriptEnvTag } from "@Packages/message/consts"; export type CompileScriptCodeResource = { name: string; @@ -134,14 +135,12 @@ export const trimScriptInfo = (script: ScriptLoadInfo): TScriptInfo => { * 将脚本函数编译为预注入脚本代码 */ export function compilePreInjectScript( - messageFlag: string, script: ScriptLoadInfo, scriptCode: string, autoDeleteMountFunction: boolean = false ): string { - const eventNamePrefix = `evt${messageFlag}${ - isInjectIntoContent(script.metadata) ? DefinedFlags.contentFlag : DefinedFlags.injectFlag - }`; + const scriptEnvTag = isInjectIntoContent(script.metadata) ? ScriptEnvTag.content : ScriptEnvTag.inject; + const eventNamePrefix = `evt${process.env.SC_RANDOM_KEY}.${scriptEnvTag}`; // 仅用于early-start初始化 const flag = `${script.flag}`; const scriptInfo = trimScriptInfo(script); const scriptInfoJSON = `${JSON.stringify(scriptInfo)}`; @@ -151,7 +150,8 @@ export function compilePreInjectScript( return `window['${flag}'] = function(){${autoDeleteMountCode}${scriptCode}}; { let o = { cancelable: true, detail: { scriptFlag: '${flag}', scriptInfo: (${scriptInfoJSON}) } }, - f = () => performance.dispatchEvent(new CustomEvent('${evScriptLoad}', o)), + c = typeof cloneInto === "function" ? cloneInto(o, performance) : o, + f = () => performance.dispatchEvent(new CustomEvent('${evScriptLoad}', c)), needWait = f(); if (needWait) performance.addEventListener('${evEnvLoad}', f, { once: true }); } diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index da64bf805..f71ae2f12 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -8,7 +8,7 @@ import type { MessageSend } from "@Packages/message/types"; import type PermissionVerify from "./permission_verify"; import { type UserConfirm } from "./permission_verify"; import { type FileSystemType } from "@Packages/filesystem/factory"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import { cacheInstance } from "@App/app/cache"; import { CACHE_KEY_IMPORT_FILE } from "@App/app/cache_key"; import { type ResourceBackup } from "@App/pkg/backup/struct"; diff --git a/src/app/service/service_worker/permission_verify.ts b/src/app/service/service_worker/permission_verify.ts index 2beefcaaf..d241287b9 100644 --- a/src/app/service/service_worker/permission_verify.ts +++ b/src/app/service/service_worker/permission_verify.ts @@ -7,7 +7,7 @@ import type { IMessageQueue } from "@Packages/message/message_queue"; import type { Api, GMApiRequest } from "./types"; import { cacheInstance } from "@App/app/cache"; import { CACHE_KEY_PERMISSION } from "@App/app/cache_key"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import Queue from "@App/pkg/utils/queue"; import { type TDeleteScript } from "../queue"; import { openInCurrentTab } from "@App/pkg/utils/utils"; diff --git a/src/app/service/service_worker/runtime.consts.ts b/src/app/service/service_worker/runtime.consts.ts index 3e8bcbf3a..0616566b4 100644 --- a/src/app/service/service_worker/runtime.consts.ts +++ b/src/app/service/service_worker/runtime.consts.ts @@ -1,8 +1,8 @@ export const DefinedFlags = { - // content 环境flag - contentFlag: ".ct", - // inject 环境flag - injectFlag: ".fd", + // Server: 回应 outbound (scripting -> content / page) + inboundFlag: ".ib", + // Client: 发送至 inbound (content / page -> scripting) + outboundFlag: ".ob", // 脚本加载完成事件 scriptLoadComplete: ".slc", // 环境加载完成事件 diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index a918ff726..f48768340 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -18,11 +18,9 @@ import { } from "./utils"; import { checkUserScriptsAvailable, - randomMessageFlag, getMetadataStr, getUserConfigStr, obtainBlackList, - isFirefox, sourceMapTo, } from "@App/pkg/utils/utils"; import { cacheInstance } from "@App/app/cache"; @@ -112,7 +110,6 @@ export class RuntimeService { sitesLoaded: Set = new Set(); updateSitesBusy: boolean = false; - loadingInitFlagsPromise: Promise | undefined; loadingInitProcessPromise: Promise | undefined; initialCompiledResourcePromise: Promise | undefined; @@ -129,13 +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(); - return this.localStorageDAO.save({ key: "scriptInjectMessageFlag", value: runtimeGlobal.messageFlag }); - }) - .catch(console.error); this.logger = LoggerCore.logger({ component: "runtime" }); // 使用中间件 @@ -328,6 +318,8 @@ export class RuntimeService { try { const res = await chrome.userScripts?.getScripts({ ids: ["scriptcat-inject"] }); registered = res?.length === 1; + } catch { + // 该错误为预期内情况,无需记录 debug 日志 } finally { // 考虑 UserScripts API 不可使用等情况 runtimeGlobal.registered = registered; @@ -553,11 +545,10 @@ export class RuntimeService { this.initReady = (async () => { // 取得初始值 或 等待各种异步同时进行的初始化 (_1, _2, ...) - const [isUserScriptsAvailable, isLoadScripts, strBlacklist, _1, _2, _3] = await Promise.all([ + const [isUserScriptsAvailable, isLoadScripts, strBlacklist, _1, _2] = await Promise.all([ checkUserScriptsAvailable(), this.systemConfig.getEnableScript(), this.systemConfig.getBlacklist(), - this.loadingInitFlagsPromise, // messageFlag 初始化等待 this.loadingInitProcessPromise, // 初始化程序等待 this.initUserAgentData(), // 初始化:userAgentData ]); @@ -676,24 +667,10 @@ 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 }), - ]); + await Promise.allSettled([chrome.userScripts?.unregister(), chrome.scripting.unregisterContentScripts()]); } } - // 生成messageFlag - generateMessageFlag(): string { - return randomMessageFlag(); - } - - getMessageFlag() { - return runtimeGlobal.messageFlag; - } - async buildAndSaveCompiledResourceFromScript(script: Script, withCode: boolean = false) { const scriptRes = withCode ? await this.script.buildScriptRunResource(script) : buildScriptRunResourceBasic(script); const resources = withCode ? scriptRes.resource : await this.resource.getScriptResources(scriptRes, true); @@ -706,12 +683,7 @@ export class RuntimeService { let jsCode = ""; if (withCode) { - const code = compileInjectionCode( - this.getMessageFlag(), - scriptRes, - scriptRes.code, - scriptMatchInfo.scriptUrlPatterns - ); + const code = compileInjectionCode(scriptRes, scriptRes.code, scriptMatchInfo.scriptUrlPatterns); registerScript.js[0].code = jsCode = code; } @@ -754,7 +726,7 @@ export class RuntimeService { if (earlyScript) { const scriptRes = await this.script.buildScriptRunResource(script); if (!scriptRes) return ""; - return compileInjectionCode(this.getMessageFlag(), scriptRes, scriptRes.code, result.scriptUrlPatterns); + return compileInjectionCode(scriptRes, scriptRes.code, result.scriptUrlPatterns); } const originalCode = await this.script.scriptCodeDAO.get(result.uuid); @@ -833,7 +805,6 @@ export class RuntimeService { excludeMatches: string[]; excludeGlobs: string[]; }) { - const messageFlag = runtimeGlobal.messageFlag; // 配置脚本运行环境: 注册时前先准备 chrome.runtime 等设定 // Firefox MV3 只提供 runtime.sendMessage 及 runtime.connect // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts/WorldProperties#messaging @@ -859,41 +830,38 @@ export class RuntimeService { const injectJs = await this.getInjectJsCode(); if (injectJs) { // 构建inject.js的脚本注册信息 - retInject = this.compileInjectUserScript(injectJs, messageFlag, { + retInject = this.compileInjectUserScript(injectJs, { excludeMatches, excludeGlobs, }); } // Note: Chrome does not support file.js?query // 注意:Chrome 不支持 file.js?query - if (isFirefox()) { - // 使用 URLSearchParams 避免字符编码问题 - retContent = [ - { - id: "scriptcat-content", - js: [`/src/content.js?${new URLSearchParams({ usp_flag: messageFlag })}&usp_end`], - matches: [""], - allFrames: true, - runAt: "document_start", - excludeMatches, - } satisfies chrome.scripting.RegisteredContentScript, - ]; - } else { - const contentJs = await this.getContentJsCode(); - if (contentJs) { - const codeBody = `(function (MessageFlag) {\n${contentJs}\n})('${messageFlag}')`; - const code = `${codeBody}${sourceMapTo("scriptcat-content.js")}\n`; - retInject.push({ - id: "scriptcat-content", - js: [{ code }], - matches: [""], - allFrames: true, - runAt: "document_start", - world: "USER_SCRIPT", - excludeMatches, - excludeGlobs, - } satisfies chrome.userScripts.RegisteredUserScript); - } + retContent = [ + { + id: "scriptcat-content", + js: ["/src/scripting.js"], + matches: [""], + allFrames: true, + runAt: "document_start", + excludeMatches, + } satisfies chrome.scripting.RegisteredContentScript, + ]; + + const contentJs = await this.getContentJsCode(); + if (contentJs) { + const codeBody = `(function () {\n${contentJs}\n})()`; + const code = `${codeBody}${sourceMapTo("scriptcat-content.js")}\n`; + retInject.push({ + id: "scriptcat-content", + js: [{ code }], + matches: [""], + allFrames: true, + runAt: "document_start", + world: "USER_SCRIPT", + excludeMatches, + excludeGlobs, + } satisfies chrome.userScripts.RegisteredUserScript); } return { content: retContent, inject: retInject }; @@ -915,6 +883,7 @@ export class RuntimeService { // scriptcat-content/scriptcat-inject不存在的情况 // 走一次重新注册的流程 this.logger.warn("registered = true but scriptcat-content/scriptcat-inject not exists, re-register userscripts."); + runtimeGlobal.registered = false; // 异常时强制反注册 } // 删除旧注册 await this.unregisterUserscripts(); @@ -980,7 +949,7 @@ export class RuntimeService { documentId: to.documentId, frameId: to.frameId, }), - "content/runtime/" + action, + "scripting/runtime/" + action, data ); } @@ -996,7 +965,7 @@ export class RuntimeService { documentId: to.documentId, frameId: to.frameId, }), - "content/runtime/emitEvent", + "scripting/runtime/emitEvent", req ); } @@ -1263,12 +1232,7 @@ export class RuntimeService { const scriptRes = scriptsWithUpdatedResources.get(targetUUID); const scriptDAOCode = scriptCodes[targetUUID]; if (scriptRes && scriptDAOCode) { - const scriptInjectCode = compileInjectionCode( - this.getMessageFlag(), - scriptRes, - scriptDAOCode, - scriptRes.scriptUrlPatterns! - ); + const scriptInjectCode = compileInjectionCode(scriptRes, scriptDAOCode, scriptRes.scriptUrlPatterns!); scriptRegisterInfo.js = [ { code: scriptInjectCode, @@ -1343,11 +1307,10 @@ export class RuntimeService { compileInjectUserScript( injectJs: string, - messageFlag: string, { excludeMatches, excludeGlobs }: { excludeMatches: string[] | undefined; excludeGlobs: string[] | undefined } ) { // 构建inject.js的脚本注册信息 - const codeBody = `(function (MessageFlag,UserAgentData) {\n${injectJs}\n})('${messageFlag}', ${JSON.stringify(this.userAgentData)})`; + const codeBody = `(function (UserAgentData) {\n${injectJs}\n})(${JSON.stringify(this.userAgentData)})`; const code = `${codeBody}${sourceMapTo("scriptcat-inject.js")}\n`; const script: chrome.userScripts.RegisteredUserScript = { id: "scriptcat-inject", diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 8fe6852b3..fe6eb1324 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -1,5 +1,5 @@ import { fetchScriptBody, parseMetadata, prepareScriptByCode } from "@App/pkg/utils/script"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import type { Group } from "@Packages/message/server"; import Logger from "@App/app/logger/logger"; import LoggerCore from "@App/app/logger/core"; diff --git a/src/app/service/service_worker/subscribe.ts b/src/app/service/service_worker/subscribe.ts index 9f7708f8f..479e9b0c1 100644 --- a/src/app/service/service_worker/subscribe.ts +++ b/src/app/service/service_worker/subscribe.ts @@ -13,7 +13,7 @@ import { checkSilenceUpdate, InfoNotification } from "@App/pkg/utils/utils"; import { ltever } from "@App/pkg/utils/semver"; import { fetchScriptBody, parseMetadata, prepareSubscribeByCode } from "@App/pkg/utils/script"; import { cacheInstance } from "@App/app/cache"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key"; import i18n, { i18nName } from "@App/locales/locales"; diff --git a/src/app/service/service_worker/utils.ts b/src/app/service/service_worker/utils.ts index 493642c17..9e7adfeba 100644 --- a/src/app/service/service_worker/utils.ts +++ b/src/app/service/service_worker/utils.ts @@ -169,7 +169,6 @@ export function parseScriptLoadInfo(script: ScriptRunResource, scriptUrlPatterns } export function compileInjectionCode( - messageFlag: string, scriptRes: ScriptRunResource, scriptCode: string, scriptUrlPatterns: URLRuleEntry[] @@ -178,11 +177,7 @@ export function compileInjectionCode( let scriptInjectCode; scriptCode = compileScriptCode(scriptRes, scriptCode); if (preDocumentStartScript) { - scriptInjectCode = compilePreInjectScript( - messageFlag, - parseScriptLoadInfo(scriptRes, scriptUrlPatterns), - scriptCode - ); + scriptInjectCode = compilePreInjectScript(parseScriptLoadInfo(scriptRes, scriptUrlPatterns), scriptCode); } else { scriptInjectCode = compileInjectScript(scriptRes, scriptCode); } diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index 42f539294..78d11587b 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -77,31 +77,20 @@ export class ValueService { // 推送值到tab async pushValueToTab(sendData: T) { - const { storageName } = sendData; - chrome.tabs.query({}, (tabs) => { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error("chrome.runtime.lastError in chrome.tabs.query:", lastError); - // 没有 tabs 资讯,无法发推送到 tabs - return; - } - // 推送到所有加载了本脚本的tab中 - for (const tab of tabs) { - const tabId = tab.id; - if (tab.discarded || !tabId) continue; - this.popup!.getScriptMenu(tabId).then((scriptMenu) => { - if (scriptMenu.find((item) => item.storageName === storageName)) { - this.runtime!.sendMessageToTab( - { - tabId, - }, - "valueUpdate", - sendData - ); - } - }); + chrome.storage.local.set( + { + valueUpdateDelivery: { + rId: `${Date.now()}.${Math.random()}`, // 用于区分不同的更新,确保 chrome.storage.local.onChanged 必能触发 + sendData, + }, + }, + () => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.runtime.lastError in chrome.storage.local.set", lastError); + } } - }); + ); // 推送到offscreen中 this.runtime!.sendMessageToTab( { diff --git a/src/content.ts b/src/content.ts index 2e4beb5c0..130de1d13 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,51 +1,32 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; -import { ExtensionMessage } from "@Packages/message/extension_message"; import { CustomEventMessage } from "@Packages/message/custom_event_message"; import { Server } from "@Packages/message/server"; -import ContentRuntime from "./app/service/content/content"; -import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_executor"; -import { randomMessageFlag, getUspMessageFlag } from "./pkg/utils/utils"; +import { 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"; -// @ts-ignore -const MessageFlag: string | null = (typeof arguments === "object" && arguments?.[0]) || getUspMessageFlag(); +const messageFlag = process.env.SC_RANDOM_KEY!; -if (!MessageFlag) { - console.error("MessageFlag is unavailable."); -} else if (typeof chrome?.runtime?.onMessage?.addListener !== "function") { - // Firefox userScripts.RegisteredUserScript does not provide chrome.runtime.onMessage.addListener - // Firefox scripting.RegisteredContentScript does provide chrome.runtime.onMessage.addListener - // Firefox 的 userScripts.RegisteredUserScript 不提供 chrome.runtime.onMessage.addListener - // Firefox 的 scripting.RegisteredContentScript 提供 chrome.runtime.onMessage.addListener - console.error("chrome.runtime.onMessage.addListener is not a function"); -} else { - // 建立与service_worker页面的连接 - const extMsgComm: Message = new ExtensionMessage(false); - // 初始化日志组件 - const loggerCore = new LoggerCore({ - writer: new MessageWriter(extMsgComm, "serviceWorker/logger"), - labels: { env: "content" }, - }); - - loggerCore.logger().debug("content start"); +getEventFlag(messageFlag, (eventFlag: string) => { + const isContent = typeof chrome.runtime?.sendMessage === "function"; + const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; - const msgInject = new CustomEventMessage(MessageFlag, true); + const msg: Message = new CustomEventMessage(`${eventFlag}${scriptEnvTag}`, false); - // 处理scriptExecutor - const scriptExecutorFlag = randomMessageFlag(); - const scriptExecutorMsg = new CustomEventMessage(scriptExecutorFlag, true); - const scriptExecutor = new ScriptExecutor(new CustomEventMessage(scriptExecutorFlag, false)); + // 初始化日志组件 + const logger = new LoggerCore({ + writer: new MessageWriter(msg, "scripting/logger"), + consoleLevel: process.env.NODE_ENV === "development" ? "debug" : "none", // 只让日志在scripting环境中打印 + labels: { env: "content", href: window.location.href }, + }); - const server = new Server("content", [msgInject, scriptExecutorMsg]); + logger.logger().debug("content start"); - // Opera中没有chrome.runtime.onConnect,并且content也不需要chrome.runtime.onConnect - // 所以不需要处理连接,设置为false - const extServer = new Server("content", extMsgComm, false); - // scriptExecutor的消息接口 - // 初始化运行环境 - const runtime = new ContentRuntime(extServer, server, extMsgComm, msgInject, scriptExecutorMsg, scriptExecutor); + const server = new Server("content", msg); + const scriptExecutor = new ScriptExecutor(msg); + const runtime = new ScriptRuntime(scriptEnvTag, server, msg, scriptExecutor, messageFlag); runtime.init(); - // 页面加载,注入脚本 - runtime.pageLoad(MessageFlag, initEnvInfo); -} +}); diff --git a/src/inject.ts b/src/inject.ts index b434858d1..08859563a 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -2,34 +2,34 @@ 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 type { TScriptInfo } from "./app/repo/scripts"; -import type { GMInfoEnv } from "./app/service/content/types"; -import { InjectRuntime } from "./app/service/content/inject"; -import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_executor"; +import { 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"; -/* global MessageFlag */ +const messageFlag = process.env.SC_RANDOM_KEY!; -const msg: Message = new CustomEventMessage(MessageFlag, false); +getEventFlag(messageFlag, (eventFlag: string) => { + const isContent = typeof chrome.runtime?.sendMessage === "function"; + const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; -// 加载logger组件 -const logger = new LoggerCore({ - writer: new MessageWriter(msg, "content/logger"), - consoleLevel: "none", // 只让日志在content环境中打印 - labels: { env: "inject", href: window.location.href }, -}); - -const server = new Server("inject", msg); -const scriptExecutor = new ScriptExecutor(msg); -const runtime = new InjectRuntime(server, msg, scriptExecutor); -runtime.init(); + const msg: Message = new CustomEventMessage(`${eventFlag}${scriptEnvTag}`, false); -// 检查early-start的脚本 -scriptExecutor.checkEarlyStartScript("inject", MessageFlag, initEnvInfo); + // 初始化日志组件 + const logger = new LoggerCore({ + writer: new MessageWriter(msg, "scripting/logger"), + consoleLevel: process.env.NODE_ENV === "development" ? "debug" : "none", // 只让日志在scripting环境中打印 + labels: { env: "inject", href: window.location.href }, + }); -server.on("pageLoad", (data: { injectScriptList: TScriptInfo[]; envInfo: GMInfoEnv }) => { logger.logger().debug("inject start"); - // 监听事件 - runtime.startScripts(data.injectScriptList, data.envInfo); - runtime.onInjectPageLoaded(); + + 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(); }); diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 7d2a6038d..d1dccfa48 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -12,7 +12,7 @@ import { Popover, } from "@arco-design/web-react"; import { IconDown } from "@arco-design/web-react/icon"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import CodeEditor from "../components/CodeEditor"; import { useEffect, useMemo, useState } from "react"; import type { SCMetadata, Script } from "@App/app/repo/scripts"; diff --git a/src/pages/options/routes/script/ScriptEditor.tsx b/src/pages/options/routes/script/ScriptEditor.tsx index 4ba5a5f0e..044cf6a09 100644 --- a/src/pages/options/routes/script/ScriptEditor.tsx +++ b/src/pages/options/routes/script/ScriptEditor.tsx @@ -10,7 +10,7 @@ import TabPane from "@arco-design/web-react/es/Tabs/tab-pane"; import normalTpl from "@App/template/normal.tpl"; import crontabTpl from "@App/template/crontab.tpl"; import backgroundTpl from "@App/template/background.tpl"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import "./index.css"; import LoggerCore from "@App/app/logger/core"; import Logger from "@App/app/logger/logger"; diff --git a/src/pkg/utils/match.test.ts b/src/pkg/utils/match.test.ts index a9c5497e7..eeec35dac 100644 --- a/src/pkg/utils/match.test.ts +++ b/src/pkg/utils/match.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { isUrlExcluded, isUrlIncluded, UrlMatch } from "./match"; -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import { extractUrlPatterns } from "./url_matcher"; describe.concurrent("UrlMatch-internal1", () => { diff --git a/src/pkg/utils/ready-wrap.ts b/src/pkg/utils/ready-wrap.ts new file mode 100644 index 000000000..ef2edb36d --- /dev/null +++ b/src/pkg/utils/ready-wrap.ts @@ -0,0 +1,16 @@ +export class ReadyWrap { + public isReady: boolean = false; + private resolve: ((value: void | PromiseLike) => void) | null = null; + private promise: Promise | null = new Promise((resolve) => { + this.resolve = resolve; + }); + onReady(fn: () => any) { + this.isReady ? fn() : this.promise!.then(fn); + } + setReady() { + this.resolve?.(); + this.isReady = true; + this.resolve = null; + this.promise = null; + } +} diff --git a/src/pkg/utils/script.ts b/src/pkg/utils/script.ts index b498976e0..35da8a242 100644 --- a/src/pkg/utils/script.ts +++ b/src/pkg/utils/script.ts @@ -1,4 +1,4 @@ -import { v4 as uuidv4 } from "uuid"; +import { uuidv4 } from "@App/pkg/utils/uuid"; import type { SCMetadata, Script, ScriptCode, UserConfig } from "@App/app/repo/scripts"; import { SCRIPT_RUN_STATUS_COMPLETE, diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index 1980ba0f0..d8b49b93a 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -39,31 +39,6 @@ export const deferred = (): Deferred => { return { promise, resolve, reject }; }; -export const getUspMessageFlag = () => { - const s = new Error().stack; - if (s) { - const search1 = "content.js?usp_flag="; - const len1 = search1.length; - const idx1 = s.indexOf(search1); - if (idx1 > 0) { - const search2 = "&usp_end"; - const idx2 = s.indexOf(search2, idx1 + len1); - if (idx2 > 0) { - const param = s.substring(idx1 + len1, idx2); - try { - // 使用 URLSearchParams 避免字符编码问题 - const uspString = `usp_flag=${param}`; - const usp = new URLSearchParams(uspString); - if (usp.size === 1) return usp.get("usp_flag") || null; - } catch (e) { - console.error(e); - } - } - } - } - return null; -}; - export function isFirefox() { //@ts-ignore return typeof mozInnerScreenX !== "undefined"; diff --git a/src/pkg/utils/uuid.ts b/src/pkg/utils/uuid.ts index d86b32696..73175e511 100644 --- a/src/pkg/utils/uuid.ts +++ b/src/pkg/utils/uuid.ts @@ -1,3 +1,3 @@ import { v4, v5 } from "uuid"; -export const uuidv4 = typeof crypto.randomUUID === "function" ? () => crypto.randomUUID() : v4; +export const uuidv4 = typeof crypto.randomUUID === "function" ? crypto.randomUUID.bind(crypto) : v4; export const uuidv5 = v5; diff --git a/src/scripting.ts b/src/scripting.ts new file mode 100644 index 000000000..9ff6d0bb1 --- /dev/null +++ b/src/scripting.ts @@ -0,0 +1,39 @@ +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"; +import { negotiateEventFlag } from "@Packages/message/common"; + +const messageFlag = process.env.SC_RANDOM_KEY!; + +// 将初始化流程完成后,将EventFlag通知到其他环境 +negotiateEventFlag(messageFlag, 2, (eventFlag) => { + // 建立与service_worker页面的连接 + const extMsgComm: Message = new ExtensionMessage(false); + // 初始化日志组件 + const logger = new LoggerCore({ + writer: new MessageWriter(extMsgComm, "serviceWorker/logger"), + labels: { env: "scripting" }, + }); + + 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(); +}); diff --git a/src/types/main.d.ts b/src/types/main.d.ts index ace032a79..f04a13904 100644 --- a/src/types/main.d.ts +++ b/src/types/main.d.ts @@ -28,8 +28,6 @@ interface FileSystemObserverInstance { observe(handle: FileSystemFileHandle | FileSystemDirectoryHandle | FileSystemSyncAccessHandle): Promise; } -declare const MessageFlag: string; - declare const UserAgentData: typeof GM_info.userAgentData; // 可以让content与inject环境交换携带dom的对象 diff --git a/tests/vitest.setup.ts b/tests/vitest.setup.ts index 3cac3508f..a992f3f32 100644 --- a/tests/vitest.setup.ts +++ b/tests/vitest.setup.ts @@ -227,16 +227,3 @@ vi.stubGlobal("define", "特殊关键字不能穿透沙盒"); if (!URL.createObjectURL) URL.createObjectURL = undefined; //@ts-expect-error if (!URL.revokeObjectURL) URL.revokeObjectURL = undefined; - -// 测试环境使用 window 代替 performance 作为 EventTarget -performance.addEventListener = function (type: string, listener: any, options?: any) { - return window.addEventListener(type, listener, options); -}; - -performance.removeEventListener = function (type: string, listener: any, options?: any) { - return window.removeEventListener(type, listener, options); -}; - -performance.dispatchEvent = function (event: Event) { - return window.dispatchEvent(event); -}; diff --git a/vitest.config.ts b/vitest.config.ts index 80250fbf1..1a82bcb1b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -29,6 +29,7 @@ export default defineConfig({ setupFiles: ["./tests/vitest.setup.ts"], env: { VI_TESTING: "true", + SC_RANDOM_KEY: "005a7deb-3a6e-4337-83ea-b9626c02ea38", }, }, });