From c43afb0a947b021e6def247051ee69855d4354b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Mon, 7 Apr 2025 18:01:44 +0800 Subject: [PATCH] =?UTF-8?q?popup=E9=A1=B5=E4=B8=8E=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/extension_message.ts | 22 ++ packages/message/server.ts | 8 +- src/app/service/offscreen/index.ts | 14 +- src/app/service/queue.ts | 16 ++ src/app/service/service_worker/client.ts | 25 ++ src/app/service/service_worker/gm_api.ts | 28 +- src/app/service/service_worker/popup.ts | 267 +++++++++++++++++- src/app/service/service_worker/runtime.ts | 206 ++++++++++---- src/app/service/service_worker/utils.ts | 42 +++ src/content.ts | 6 +- src/manifest.json | 1 + src/pages/components/ScriptMenuList/index.tsx | 110 ++------ src/pages/popup/App.tsx | 72 +++-- src/pages/store/features/script.ts | 3 +- src/pages/store/features/setting.ts | 7 +- src/runtime/content/content.ts | 178 +++--------- src/runtime/content/gm_api.ts | 21 +- 17 files changed, 667 insertions(+), 359 deletions(-) diff --git a/packages/message/extension_message.ts b/packages/message/extension_message.ts index 9b33fa3..f9104dd 100644 --- a/packages/message/extension_message.ts +++ b/packages/message/extension_message.ts @@ -84,3 +84,25 @@ export class ExtensionMessageConnect implements MessageConnect { this.con.onDisconnect.addListener(callback); } } + +export class ExtensionContentMessageSend extends ExtensionMessageSend { + constructor(private tabId: number) { + super(); + } + + sendMessage(data: any): Promise { + return new Promise((resolve) => { + chrome.tabs.sendMessage(this.tabId, data, (resp) => { + resolve(resp); + }); + }); + } + + connect(data: any): Promise { + return new Promise((resolve) => { + const con = chrome.tabs.connect(this.tabId); + con.postMessage(data); + resolve(new ExtensionMessageConnect(con)); + }); + } +} diff --git a/packages/message/server.ts b/packages/message/server.ts index 90237aa..0bf8f12 100644 --- a/packages/message/server.ts +++ b/packages/message/server.ts @@ -17,7 +17,7 @@ export interface MessageConnect { onDisconnect(callback: () => void): void; } -export type MessageSender = any; +export type MessageSender = chrome.runtime.MessageSender; export class GetSender { constructor(private sender: MessageConnect | MessageSender) {} @@ -117,13 +117,13 @@ export class Group { export function forwardMessage(prefix: string, path: string, from: Server, to: MessageSend, middleware?: ApiFunction) { from.on(path, async (params, fromCon) => { if (middleware) { - const resp = await middleware(params, new GetSender(fromCon)); + const resp = await middleware(params, fromCon); if (resp !== false) { return resp; } } - if (fromCon) { - const fromConnect = fromCon.getConnect(); + const fromConnect = fromCon.getConnect(); + if (fromConnect) { to.connect({ action: prefix + "/" + path, data: params }).then((toCon) => { fromConnect.onMessage((data) => { toCon.sendMessage(data); diff --git a/src/app/service/offscreen/index.ts b/src/app/service/offscreen/index.ts index e9865c8..b68b72f 100644 --- a/src/app/service/offscreen/index.ts +++ b/src/app/service/offscreen/index.ts @@ -14,7 +14,7 @@ export class OffscreenManager { private windowMessage = new WindowMessage(window, sandbox, true); - private windowApi: Server = new Server("offscreen", this.windowMessage); + private windowServer: Server = new Server("offscreen", this.windowMessage); private messageQueue: MessageQueue = new MessageQueue(); @@ -36,21 +36,21 @@ export class OffscreenManager { async initManager() { // 监听消息 - this.windowApi.on("logger", this.logger.bind(this)); - this.windowApi.on("preparationSandbox", this.preparationSandbox.bind(this)); - this.windowApi.on("sendMessageToServiceWorker", this.sendMessageToServiceWorker.bind(this)); + this.windowServer.on("logger", this.logger.bind(this)); + this.windowServer.on("preparationSandbox", this.preparationSandbox.bind(this)); + this.windowServer.on("sendMessageToServiceWorker", this.sendMessageToServiceWorker.bind(this)); const script = new ScriptService( - this.windowApi.group("script"), + this.windowServer.group("script"), this.extensionMessage, this.windowMessage, this.messageQueue ); script.init(); // 转发从sandbox来的gm api请求 - forwardMessage("serviceWorker", "runtime/gmApi", this.windowApi, this.extensionMessage); + forwardMessage("serviceWorker", "runtime/gmApi", this.windowServer, this.extensionMessage); // 转发message queue请求 - const gmApi = new GMApi(this.windowApi.group("gmApi")); + const gmApi = new GMApi(this.windowServer.group("gmApi")); gmApi.init(); } } diff --git a/src/app/service/queue.ts b/src/app/service/queue.ts index ef1caa7..08f1b7d 100644 --- a/src/app/service/queue.ts +++ b/src/app/service/queue.ts @@ -27,3 +27,19 @@ export function subscribeScriptRunStatus( ) { return messageQueue.subscribe("scriptRunStatus", callback); } + +export type ScriptMenuRegisterCallbackValue = { + uuid: string; + id: number; + name: string; + accessKey: string; + tabId: number; + frameId: number; +}; + +export function subscribeScriptMenuRegister( + messageQueue: MessageQueue, + callback: (message: ScriptMenuRegisterCallbackValue) => void +) { + return messageQueue.subscribe("registerMenuCommand", callback); +} diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index 9bce727..3cc2c39 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -3,6 +3,7 @@ import { Client } from "@Packages/message/client"; import { InstallSource } from "."; import { Resource } from "@App/app/repo/resource"; import { MessageSend } from "@Packages/message/server"; +import { ScriptMenu, ScriptMenuItem } from "./popup"; export class ServiceWorkerClient extends Client { constructor(msg: MessageSend) { @@ -90,3 +91,27 @@ export class RuntimeClient extends Client { return this.do("scriptLoad", { flag, uuid }); } } + +export type GetPopupDataReq = { + tabId: number; + url: string; +}; + +export type GetPopupDataRes = { + scriptList: ScriptMenu[]; + backScriptList: ScriptMenu[]; +}; + +export class PopupClient extends Client { + constructor(msg: MessageSend) { + super(msg, "serviceWorker/popup"); + } + + getPopupData(data: GetPopupDataReq): Promise { + return this.do("getPopupData", data); + } + + menuClick(uuid: string, data: ScriptMenuItem) { + return this.do("menuClick", { uuid, id: data.id, tabId: data.tabId, frameId: data.frameId }); + } +} diff --git a/src/app/service/service_worker/gm_api.ts b/src/app/service/service_worker/gm_api.ts index 0e4cab4..3d9b79c 100644 --- a/src/app/service/service_worker/gm_api.ts +++ b/src/app/service/service_worker/gm_api.ts @@ -1,7 +1,7 @@ import LoggerCore from "@App/app/logger/core"; import Logger from "@App/app/logger/logger"; import { Script, ScriptDAO } from "@App/app/repo/scripts"; -import { GetSender, Group, MessageSend, MessageSender } from "@Packages/message/server"; +import { GetSender, Group, MessageSend } from "@Packages/message/server"; import { ValueService } from "@App/app/service/service_worker/value"; import PermissionVerify from "./permission_verify"; import { connect } from "@Packages/message/client"; @@ -21,7 +21,6 @@ export type MessageRequest = { export type Request = MessageRequest & { script: Script; - sender: MessageSender; }; export type Api = (request: Request, con: GetSender) => Promise; @@ -48,7 +47,7 @@ export default class GMApi { if (!api) { return Promise.reject(new Error("gm api is not found")); } - const req = await this.parseRequest(data, { tabId: 0 }); + const req = await this.parseRequest(data); try { await this.permissionVerify.verify(req, api); } catch (e) { @@ -59,14 +58,13 @@ export default class GMApi { } // 解析请求 - async parseRequest(data: MessageRequest, sender: MessageSender): Promise { + async parseRequest(data: MessageRequest): Promise { const script = await this.scriptDAO.get(data.uuid); if (!script) { return Promise.reject(new Error("script is not found")); } const req: Request = data; req.script = script; - req.sender = sender; return Promise.resolve(req); } @@ -76,8 +74,6 @@ export default class GMApi { return Promise.reject(new Error("param is failed")); } const [key, value] = request.params; - const sender = request.sender; - sender.runFlag = request.runFlag; return this.value.setValue(request.script.uuid, key, value); } @@ -183,8 +179,8 @@ export default class GMApi { } @PermissionVerify.API() - GM_registerMenuCommand(request: Request, con: GetSender) { - console.log("registerMenuCommand", request.params); + GM_registerMenuCommand(request: Request, sender: GetSender) { + console.log("registerMenuCommand", request.params, sender.getSender(), sender.getSender().tab!.id!); const [id, name, accessKey] = request.params; // 触发菜单注册, 在popup中处理 this.mq.emit("registerMenuCommand", { @@ -192,24 +188,20 @@ export default class GMApi { id: id, name: name, accessKey: accessKey, - con: con.getConnect(), - }); - con.getConnect().onDisconnect(() => { - // 取消注册 - this.mq.emit("unregisterMenuCommand", { - uuid: request.script.uuid, - name: name, - }); + tabId: sender.getSender().tab!.id!, + frameId: sender.getSender().frameId, }); } @PermissionVerify.API() - GM_unregisterMenuCommand(request: Request) { + GM_unregisterMenuCommand(request: Request, sender: GetSender) { const [id] = request.params; // 触发菜单取消注册, 在popup中处理 this.mq.emit("unregisterMenuCommand", { uuid: request.script.uuid, id: id, + tabId: sender.getSender().tab!.id!, + frameId: sender.getSender().frameId, }); } diff --git a/src/app/service/service_worker/popup.ts b/src/app/service/service_worker/popup.ts index 1ae17c0..4061d87 100644 --- a/src/app/service/service_worker/popup.ts +++ b/src/app/service/service_worker/popup.ts @@ -1,26 +1,279 @@ import { MessageQueue } from "@Packages/message/message_queue"; -import { GetSender, Group } from "@Packages/message/server"; -import { RuntimeService } from "./runtime"; +import { Group } from "@Packages/message/server"; +import { RuntimeService, ScriptMatchInfo } from "./runtime"; +import Cache from "@App/app/cache"; +import { GetPopupDataReq, GetPopupDataRes } from "./client"; +import { + SCRIPT_RUN_STATUS, + Metadata, + SCRIPT_STATUS_ENABLE, + Script, + ScriptDAO, + SCRIPT_TYPE_NORMAL, +} from "@App/app/repo/scripts"; +import { + ScriptMenuRegisterCallbackValue, + subscribeScriptDelete, + subscribeScriptEnable, + subscribeScriptInstall, + subscribeScriptMenuRegister, + subscribeScriptRunStatus, +} from "../queue"; + +export type ScriptMenuItem = { + id: number; + name: string; + accessKey?: string; + tabId: number | "background"; + frameId: number; +}; + +export type ScriptMenu = { + uuid: string; // 脚本uuid + name: string; // 脚本名称 + enable: boolean; // 脚本是否启用 + updatetime: number; // 脚本更新时间 + hasUserConfig: boolean; // 是否有用户配置 + metadata: Metadata; // 脚本元数据 + runStatus?: SCRIPT_RUN_STATUS; // 脚本运行状态 + runNum: number; // 脚本运行次数 + runNumByIframe: number; // iframe运行次数 + menus: ScriptMenuItem[]; // 脚本菜单 + customExclude: string[]; // 自定义排除 +}; // 处理popup页面的数据 export class PopupService { + scriptDAO = new ScriptDAO(); + constructor( private group: Group, private mq: MessageQueue, private runtime: RuntimeService ) {} - registerMenuCommand(message: { uuid: string; id: string; name: string; accessKey: string; con: GetSender }) { - console.log("registerMenuCommand", message); + async registerMenuCommand(message: ScriptMenuRegisterCallbackValue) { + // 给脚本添加菜单 + const data = await this.getScriptMenu(message.tabId); + const script = data.find((item) => item.uuid === message.uuid); + if (script) { + const menu = script.menus.find((item) => item.id === message.id); + if (!menu) { + script.menus.push({ + id: message.id, + name: message.name, + accessKey: message.accessKey, + tabId: message.tabId, + frameId: message.frameId, + }); + } else { + menu.name = message.name; + menu.accessKey = message.accessKey; + menu.tabId = message.tabId; + } + } + console.log(data); + Cache.getInstance().set("tabScript:" + message.tabId, data); } - unregisterMenuCommand(message: { id: string }) { - console.log("unregisterMenuCommand", message); + async unregisterMenuCommand({ id, uuid, tabId }: { id: number; uuid: string; tabId: number }) { + const data = await this.getScriptMenu(tabId); + // 删除脚本菜单 + const script = data.find((item) => item.uuid === uuid); + if (script) { + script.menus = script.menus.filter((item) => item.id !== id); + } + Cache.getInstance().set("tabScript:" + tabId, data); + } + + scriptToMenu(script: Script): ScriptMenu { + return { + uuid: script.uuid, + name: script.name, + enable: script.status === SCRIPT_STATUS_ENABLE, + updatetime: script.updatetime || 0, + hasUserConfig: !!script.config, + metadata: script.metadata, + runStatus: script.runStatus, + runNum: 0, + runNumByIframe: 0, + menus: [], + customExclude: (script as ScriptMatchInfo).customizeExcludeMatches || [], + }; + } + + // 获取popup页面数据 + async getPopupData(req: GetPopupDataReq): Promise { + // 获取当前tabId + const scriptUuid = await this.runtime.getPageScriptByUrl(req.url); + // 与运行时脚本进行合并 + const runScript = await this.getScriptMenu(req.tabId); + // 筛选出未运行的脚本 + const notRunScript = scriptUuid.filter((script) => { + return !runScript.find((item) => item.uuid === script.uuid); + }); + // 将未运行的脚本转换为菜单 + const scriptList = notRunScript.map((script): ScriptMenu => { + return this.scriptToMenu(script); + }); + runScript.push(...scriptList); + // 后台脚本只显示开启或者运行中的脚本 + + return { scriptList: runScript, backScriptList: await this.getScriptMenu(-1) }; + } + + async getScriptMenu(tabId: number) { + return ((await Cache.getInstance().get("tabScript:" + tabId)) || []) as ScriptMenu[]; + } + + async addScriptRunNumber({ + tabId, + frameId, + scripts, + }: { + tabId: number; + frameId: number; + scripts: ScriptMatchInfo[]; + }) { + if (frameId === undefined) { + // 清理数据 + await Cache.getInstance().del("tabScript:" + tabId); + } + // 设置数据 + const data = await this.getScriptMenu(tabId); + // 设置脚本运行次数 + scripts.forEach((script) => { + const scriptMenu = data.find((item) => item.uuid === script.uuid); + if (scriptMenu) { + scriptMenu.runNum = (scriptMenu.runNum || 0) + 1; + if (frameId) { + scriptMenu.runNumByIframe = (scriptMenu.runNumByIframe || 0) + 1; + } + } else { + const item = this.scriptToMenu(script); + item.runNum = 1; + if (frameId) { + item.runNumByIframe = 1; + } + data.push(item); + } + }); + Cache.getInstance().set("tabScript:" + tabId, data); + } + + dealBackgroundScriptInstall() { + // 处理后台脚本 + subscribeScriptInstall(this.mq, async ({ script }) => { + if (script.type === SCRIPT_TYPE_NORMAL) { + return; + } + const menu = await this.getScriptMenu(-1); + const scriptMenu = menu.find((item) => item.uuid === script.uuid); + if (script.status === SCRIPT_STATUS_ENABLE) { + // 加入菜单 + if (!scriptMenu) { + const item = this.scriptToMenu(script); + menu.push(item); + } + } else { + // 移出菜单 + if (scriptMenu) { + menu.splice(menu.indexOf(scriptMenu), 1); + } + } + Cache.getInstance().set("tabScript:" + -1, menu); + }); + subscribeScriptEnable(this.mq, async ({ uuid }) => { + const script = await this.scriptDAO.get(uuid); + if (!script) { + return; + } + if (script.type === SCRIPT_TYPE_NORMAL) { + return; + } + const menu = await this.getScriptMenu(-1); + const scriptMenu = menu.find((item) => item.uuid === uuid); + if (script.status === SCRIPT_STATUS_ENABLE) { + // 加入菜单 + if (!scriptMenu) { + const item = this.scriptToMenu(script); + menu.push(item); + } + } else { + // 移出菜单 + if (scriptMenu) { + menu.splice(menu.indexOf(scriptMenu), 1); + } + } + Cache.getInstance().set("tabScript:" + -1, menu); + }); + subscribeScriptDelete(this.mq, async ({ uuid }) => { + const menu = await this.getScriptMenu(-1); + const scriptMenu = menu.find((item) => item.uuid === uuid); + if (scriptMenu) { + menu.splice(menu.indexOf(scriptMenu), 1); + Cache.getInstance().set("tabScript:" + -1, menu); + } + }); + subscribeScriptRunStatus(this.mq, async ({ uuid, runStatus }) => { + const menu = await this.getScriptMenu(-1); + const scriptMenu = menu.find((item) => item.uuid === uuid); + if (scriptMenu) { + scriptMenu.runStatus = runStatus; + Cache.getInstance().set("tabScript:" + -1, menu); + } + }); + } + + menuClick({ uuid, id, tabId, frameId }: { uuid: string; id: number; tabId: number; frameId: number }) { + // 菜单点击事件 + console.log("click menu", uuid, id, tabId); + this.runtime.sendMessageToTab(tabId, "menuClick", { + uuid, + id, + tabId, + frameId, + }); + return Promise.resolve(true); } init() { // 处理脚本菜单数据 - this.mq.subscribe("registerMenuCommand", this.registerMenuCommand.bind(this)); + subscribeScriptMenuRegister(this.mq, this.registerMenuCommand.bind(this)); this.mq.subscribe("unregisterMenuCommand", this.unregisterMenuCommand.bind(this)); + this.group.on("getPopupData", this.getPopupData.bind(this)); + this.group.on("menuClick", this.menuClick.bind(this)); + this.dealBackgroundScriptInstall(); + + // 监听tab开关 + chrome.tabs.onRemoved.addListener((tabId) => { + // 清理数据 + Cache.getInstance().del("tabScript:" + tabId); + }); + // 监听运行次数 + this.mq.subscribe( + "pageLoad", + async ({ tabId, frameId, scripts }: { tabId: number; frameId: number; scripts: ScriptMatchInfo[] }) => { + this.addScriptRunNumber({ tabId, frameId, scripts }); + // 设置角标和脚本 + chrome.action.getBadgeText( + { + tabId: tabId, + }, + (res: string) => { + if (res || scripts.length) { + chrome.action.setBadgeText({ + text: (scripts.length + (parseInt(res, 10) || 0)).toString(), + tabId: tabId, + }); + chrome.action.setBadgeBackgroundColor({ + color: "#4e5969", + tabId: tabId, + }); + } + } + ); + } + ); } } diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index c421027..2ea220e 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -1,6 +1,14 @@ import { MessageQueue } from "@Packages/message/message_queue"; import { GetSender, Group, MessageSend } from "@Packages/message/server"; -import { Script, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL, ScriptDAO, ScriptRunResouce } from "@App/app/repo/scripts"; +import { + Script, + SCRIPT_STATUS, + SCRIPT_STATUS_DISABLE, + SCRIPT_STATUS_ENABLE, + SCRIPT_TYPE_NORMAL, + ScriptDAO, + ScriptRunResouce, +} from "@App/app/repo/scripts"; import { ValueService } from "./value"; import GMApi from "./gm_api"; import { subscribeScriptDelete, subscribeScriptEnable, subscribeScriptInstall } from "../queue"; @@ -11,17 +19,21 @@ import { randomString } from "@App/pkg/utils/utils"; import { compileInjectScript } from "@App/runtime/content/utils"; import Cache from "@App/app/cache"; import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match"; +import { ExtensionContentMessageSend } from "@Packages/message/extension_message"; +import { sendMessage } from "@Packages/message/client"; // 为了优化性能,存储到缓存时删除了code与value export interface ScriptMatchInfo extends ScriptRunResouce { matches: string[]; excludeMatches: string[]; + customizeExcludeMatches: string[]; } export class RuntimeService { scriptDAO: ScriptDAO = new ScriptDAO(); scriptMatch: UrlMatch = new UrlMatch(); + scriptCustomizeMatch: UrlMatch = new UrlMatch(); scriptMatchCache: Map | null | undefined; constructor( @@ -52,11 +64,10 @@ export class RuntimeService { // 如果是普通脚本, 在service worker中进行注册 // 如果是后台脚本, 在offscreen中进行处理 if (script.type === SCRIPT_TYPE_NORMAL) { - // 注册入页面脚本 - if (data.enable) { - this.registryPageScript(script); - } else { - this.unregistryPageScript(script); + // 加载页面脚本 + await this.loadPageScript(script); + if (!data.enable) { + await this.unregistryPageScript(script); } } }); @@ -67,7 +78,7 @@ export class RuntimeService { return; } if (script.type === SCRIPT_TYPE_NORMAL) { - this.registryPageScript(script); + await this.loadPageScript(script); } }); // 监听脚本删除 @@ -77,7 +88,8 @@ export class RuntimeService { return; } if (script.type === SCRIPT_TYPE_NORMAL) { - this.unregistryPageScript(script); + await this.unregistryPageScript(script); + this.deleteScriptMatch(script.uuid); } }); @@ -85,20 +97,22 @@ export class RuntimeService { const scriptDao = new ScriptDAO(); const list = await scriptDao.all(); list.forEach((script) => { - if (script.status !== SCRIPT_STATUS_ENABLE || script.type !== SCRIPT_TYPE_NORMAL) { + if (script.type !== SCRIPT_TYPE_NORMAL) { return; } - this.mq.publish("enableScript", { uuid: script.uuid, enable: true }); + this.mq.publish("enableScript", { uuid: script.uuid, enable: script.status === SCRIPT_STATUS_ENABLE }); }); // 监听offscreen环境初始化, 初始化完成后, 再将后台脚本运行起来 this.mq.subscribe("preparationOffscreen", () => { list.forEach((script) => { - if (script.status !== SCRIPT_STATUS_ENABLE || script.type === SCRIPT_TYPE_NORMAL) { + if (script.type === SCRIPT_TYPE_NORMAL) { return; } - this.mq.publish("enableScript", { uuid: script.uuid, enable: true }); + this.mq.publish("enableScript", { uuid: script.uuid, enable: script.status === SCRIPT_STATUS_ENABLE }); }); }); + + this.loadScriptMatchInfo(); } messageFlag() { @@ -107,23 +121,72 @@ export class RuntimeService { }); } - async pageLoad(_, sender: GetSender) { - const [scriptFlag, match] = await Promise.all([this.messageFlag(), this.loadScriptMatchInfo()]); + // 给指定tab发送消息 + sendMessageToTab(tabId: number, action: string, data: any) { + if (tabId === -1) { + // 如果是-1, 代表给offscreen发送消息 + return sendMessage(this.sender, "offscreen/runtime/" + action, data); + } + return sendMessage(new ExtensionContentMessageSend(tabId), "content/runtime/" + action, data); + } + + async getPageScriptUuidByUrl(url: string) { + const match = await this.loadScriptMatchInfo(); + // 匹配当前页面的脚本 + const matchScriptUuid = match.match(url!); + // 排除自定义匹配 + const excludeScriptUuid = this.scriptCustomizeMatch.match(url!); + const excludeMatch = new Set(); + excludeScriptUuid.forEach((uuid) => { + excludeMatch.add(uuid); + }); + return matchScriptUuid.filter((value) => { + // 过滤掉自定义排除的脚本 + return !excludeMatch.has(value); + }); + } + + async getPageScriptByUrl(url: string) { + const matchScriptUuid = await this.getPageScriptUuidByUrl(url); + return matchScriptUuid.map((uuid) => { + return Object.assign({}, this.scriptMatchCache?.get(uuid)); + }); + } + + async pageLoad(_: any, sender: GetSender) { + const [scriptFlag] = await Promise.all([this.messageFlag(), this.loadScriptMatchInfo()]); const chromeSender = sender.getSender() as chrome.runtime.MessageSender; // 匹配当前页面的脚本 - const matchScriptUuid = match.match(chromeSender.url!); + const matchScriptUuid = await this.getPageScriptUuidByUrl(chromeSender.url!); + const scripts = await Promise.all( - matchScriptUuid.map( - (uuid) => - new Promise((resolve) => { - // 获取value - const scriptRes = Object.assign({}, this.scriptMatchCache?.get(uuid)); - resolve(scriptRes); - }) - ) + matchScriptUuid.map(async (uuid): Promise => { + const scriptRes = Object.assign({}, this.scriptMatchCache?.get(uuid)); + // 判断脚本是否开启 + if (scriptRes.status === SCRIPT_STATUS_DISABLE) { + return undefined; + } + // 如果是iframe,判断是否允许在iframe里运行 + if (chromeSender.frameId !== undefined) { + if (scriptRes.metadata.noframes) { + return undefined; + } + } + // 获取value + return scriptRes; + }) ); - return Promise.resolve({ flag: scriptFlag, scripts: scripts }); + + const enableScript = scripts.filter((item) => item); + + this.mq.emit("pageLoad", { + tabId: chromeSender.tab?.id, + frameId: chromeSender.frameId, + scripts: enableScript, + }); + + return Promise.resolve({ flag: scriptFlag, scripts: enableScript }); } // 停止脚本 @@ -174,7 +237,7 @@ export class RuntimeService { }); } - loadScripting: Promise | null | undefined; + loadingScript: Promise | null | undefined; // 加载脚本匹配信息,由于service_worker的机制,如果由不活动状态恢复过来时,会优先触发事件 // 可能当时会没有脚本匹配信息,所以使用脚本信息时,尽量使用此方法获取 @@ -182,29 +245,33 @@ export class RuntimeService { if (this.scriptMatchCache) { return this.scriptMatch; } - if (this.loadScripting) { - await this.loadScripting; + if (this.loadingScript) { + await this.loadingScript; } else { // 如果没有缓存, 则创建一个新的缓存 - this.loadScripting = Cache.getInstance() + const cache = new Map(); + this.loadingScript = Cache.getInstance() .get("scriptMatch") .then((data: { [key: string]: ScriptMatchInfo }) => { - this.scriptMatchCache = new Map(); if (data) { Object.keys(data).forEach((key) => { const item = data[key]; - this.scriptMatchCache!.set(item.uuid, item); + cache.set(item.uuid, item); item.matches.forEach((match) => { this.scriptMatch.add(match, item.uuid); }); item.excludeMatches.forEach((match) => { this.scriptMatch.exclude(match, item.uuid); }); + item.customizeExcludeMatches.forEach((match) => { + this.scriptCustomizeMatch.exclude(match, item.uuid); + }); }); } }); - await this.loadScripting; - this.loadScripting = null; + await this.loadingScript; + this.loadingScript = null; + this.scriptMatchCache = cache; } return this.scriptMatch; } @@ -229,28 +296,43 @@ export class RuntimeService { await this.loadScriptMatchInfo(); } this.scriptMatchCache!.set(item.uuid, item); + // 清理一下老数据 + this.scriptMatch.del(item.uuid); + this.scriptCustomizeMatch.del(item.uuid); + // 添加新的数据 item.matches.forEach((match) => { this.scriptMatch.add(match, item.uuid); }); item.excludeMatches.forEach((match) => { this.scriptMatch.exclude(match, item.uuid); }); + item.customizeExcludeMatches.forEach((match) => { + this.scriptCustomizeMatch.exclude(match, item.uuid); + }); this.saveScriptMatchInfo(); } - async deleteScriptMatch(uuid: string) { + async updateScriptStatus(uuid: string, status: SCRIPT_STATUS) { if (!this.scriptMatchCache) { await this.loadScriptMatchInfo(); } - this.scriptMatchCache!.delete(uuid); - this.scriptMatch.del(uuid); + this.scriptMatchCache!.get(uuid)!.status = status; this.saveScriptMatchInfo(); } - async registryPageScript(script: Script) { - if (await Cache.getInstance().has("registryScript:" + script.uuid)) { + deleteScriptMatch(uuid: string) { + if (!this.scriptMatchCache) { return; } + this.scriptMatchCache.delete(uuid); + this.scriptMatch.del(uuid); + this.scriptCustomizeMatch.del(uuid); + this.saveScriptMatchInfo(); + } + + // 加载页面脚本, 会把脚本信息放入缓存中 + // 如果脚本开启, 则注册脚本 + async loadPageScript(script: Script) { const matches = script.metadata["match"]; if (!matches) { return; @@ -262,7 +344,7 @@ export class RuntimeService { matches.push(...(script.metadata["include"] || [])); const patternMatches = dealPatternMatches(matches); const scriptMatchInfo: ScriptMatchInfo = Object.assign( - { matches: patternMatches.result, excludeMatches: [] }, + { matches: patternMatches.result, excludeMatches: [], customizeExcludeMatches: [] }, scriptRes ); @@ -272,30 +354,46 @@ export class RuntimeService { matches: patternMatches.patternResult, world: "MAIN", }; - if (!script.metadata["noframes"]) { - registerScript.allFrames = true; - } - if (script.metadata["exclude-match"]) { - const excludeMatches = script.metadata["exclude-match"]; - excludeMatches.push(...(script.metadata["exclude"] || [])); + if (script.metadata["exclude"]) { + const excludeMatches = script.metadata["exclude"]; const result = dealPatternMatches(excludeMatches); registerScript.excludeMatches = result.patternResult; scriptMatchInfo.excludeMatches = result.result; } - if (script.metadata["run-at"]) { - registerScript.runAt = getRunAt(script.metadata["run-at"]); + // 自定义排除 + if (script.selfMetadata && script.selfMetadata.exclude) { + const excludeMatches = script.selfMetadata.exclude; + const result = dealPatternMatches(excludeMatches); + + if (!registerScript.excludeMatches) { + registerScript.excludeMatches = []; + } + registerScript.excludeMatches.push(...result.patternResult); + scriptMatchInfo.customizeExcludeMatches = result.result; + } + + // 将脚本match信息放入缓存中 + this.addScriptMatch(scriptMatchInfo); + + // 如果脚本开启, 则注册脚本 + if (script.status === SCRIPT_STATUS_ENABLE) { + if (!script.metadata["noframes"]) { + registerScript.allFrames = true; + } + if (script.metadata["run-at"]) { + registerScript.runAt = getRunAt(script.metadata["run-at"]); + } + await chrome.userScripts.register([registerScript]); + await Cache.getInstance().set("registryScript:" + script.uuid, true); } - chrome.userScripts.register([registerScript], async () => { - // 标记为已注册 - Cache.getInstance().set("registryScript:" + script.uuid, true); - // 将脚本match信息放入缓存中 - this.addScriptMatch(scriptMatchInfo); - }); } - unregistryPageScript(script: Script) { + async unregistryPageScript(script: Script) { + if (!(await Cache.getInstance().get("registryScript:" + script.uuid))) { + return; + } chrome.userScripts.unregister( { ids: [script.uuid], @@ -303,6 +401,8 @@ export class RuntimeService { () => { // 删除缓存 Cache.getInstance().del("registryScript:" + script.uuid); + // 修改脚本状态为disable + this.updateScriptStatus(script.uuid, SCRIPT_STATUS_DISABLE); } ); } diff --git a/src/app/service/service_worker/utils.ts b/src/app/service/service_worker/utils.ts index eb68ab4..013ed84 100644 --- a/src/app/service/service_worker/utils.ts +++ b/src/app/service/service_worker/utils.ts @@ -17,3 +17,45 @@ export function getRunAt(runAts: string[]): chrome.userScripts.RunAt { } return "document_idle"; } + +export function mapToObject(map: Map): { [key: string]: any } { + const obj: { [key: string]: any } = {}; + map.forEach((value, key) => { + if (value instanceof Map) { + obj[key] = mapToObject(value); + } else if (obj[key] instanceof Array) { + obj[key].push(value); + } else { + obj[key] = value; + } + }); + return obj; +} + +export function objectToMap(obj: { [key: string]: any }): Map { + const map = new Map(); + Object.keys(obj).forEach((key) => { + if (obj[key] instanceof Map) { + map.set(key, objectToMap(obj[key])); + } else if (obj[key] instanceof Array) { + map.set(key, obj[key]); + } else { + map.set(key, obj[key]); + } + }); + return map; +} + +export function arrayToObject(arr: Array): any[] { + const obj: any[] = []; + arr.forEach((item) => { + if (item instanceof Map) { + obj.push(mapToObject(item)); + } else if (item instanceof Array) { + obj.push(arrayToObject(item)); + } else { + obj.push(item); + } + }); + return obj; +} diff --git a/src/content.ts b/src/content.ts index d09d009..02ddd5a 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,6 +1,6 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; -import { ExtensionMessageSend } from "@Packages/message/extension_message"; +import { ExtensionMessage, ExtensionMessageSend } from "@Packages/message/extension_message"; import { CustomEventMessage } from "@Packages/message/custom_event_message"; import { RuntimeClient } from "./app/service/service_worker/client"; import ContentRuntime from "./runtime/content/content"; @@ -18,9 +18,11 @@ const loggerCore = new LoggerCore({ const client = new RuntimeClient(send); client.pageLoad().then((data) => { loggerCore.logger().debug("content start"); + const extMsg = new ExtensionMessage(); const msg = new CustomEventMessage(data.flag, true); const server = new Server("content", msg); + const extServer = new Server("content", extMsg); // 初始化运行环境 - const runtime = new ContentRuntime(server, send, msg); + const runtime = new ContentRuntime(extServer, server, send, msg); runtime.start(data.scripts); }); diff --git a/src/manifest.json b/src/manifest.json index c86dc3e..fd07827 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -23,6 +23,7 @@ "default_locale": "zh_CN", "permissions": [ "tabs", + "action", "storage", "offscreen", "scripting", diff --git a/src/pages/components/ScriptMenuList/index.tsx b/src/pages/components/ScriptMenuList/index.tsx index c4c91e8..5ae1491 100644 --- a/src/pages/components/ScriptMenuList/index.tsx +++ b/src/pages/components/ScriptMenuList/index.tsx @@ -1,15 +1,5 @@ -/* eslint-disable no-nested-ternary */ import React, { useEffect, useState } from "react"; -import { ScriptMenu } from "@App/runtime/service_worker/runtime"; -import { - Button, - Collapse, - Empty, - Message, - Popconfirm, - Space, - Switch, -} from "@arco-design/web-react"; +import { Button, Collapse, Empty, Message, Popconfirm, Space, Switch } from "@arco-design/web-react"; import { IconCaretDown, IconCaretUp, @@ -23,6 +13,11 @@ import { SCRIPT_RUN_STATUS_RUNNING } from "@App/app/repo/scripts"; import { RiPlayFill, RiStopFill } from "react-icons/ri"; import { useTranslation } from "react-i18next"; import { ScriptIcons } from "@App/pages/options/routes/utils"; +import { ScriptMenu, ScriptMenuItem } from "@App/app/service/service_worker/popup"; +import { MessageSender } from "@Packages/message/server"; +import { selectMenuExpandNum } from "@App/pages/store/features/setting"; +import { useAppSelector } from "@App/pages/store/hooks"; +import { popupClient } from "@App/pages/store/features/script"; const CollapseItem = Collapse.Item; @@ -49,12 +44,13 @@ const ScriptMenuList: React.FC<{ [key: string]: boolean; }>({}); const { t } = useTranslation(); + const menuExpandNum = useAppSelector(selectMenuExpandNum); let url: URL; try { url = new URL(currentUrl); - } catch (e) { - // ignore error + } catch (e: any) { + console.error("Invalid URL:", e); } useEffect(() => { setList(script); @@ -78,20 +74,10 @@ const ScriptMenuList: React.FC<{ // }; // }, []); - const sendMenuAction = (sender: MessageSender, channelFlag: string) => { - let id = sender.tabId; - if (sender.frameId) { - id = sender.frameId; - } - message.broadcastChannel( - { - tag: sender.targetTag, - id: [id!], - }, - channelFlag, - "click" - ); - window.close(); + const sendMenuAction = (uuid: string, menu: ScriptMenuItem) => { + popupClient.menuClick(uuid, menu).then(() => { + window.close(); + }); }; // 监听菜单按键 @@ -101,16 +87,14 @@ const ScriptMenuList: React.FC<{ <> {list.length === 0 && } {list.map((item, index) => ( - + { e.stopPropagation(); }} title={ - // eslint-disable-next-line no-nested-ternary item.enable ? item.runNumByIframe ? t("script_total_runs", { @@ -159,7 +143,7 @@ const ScriptMenuList: React.FC<{ } - name={item.id.toString()} + name={item.uuid} contentStyle={{ padding: "0 0 0 40px" }} >
@@ -167,13 +151,7 @@ const ScriptMenuList: React.FC<{ )} )} @@ -235,32 +200,24 @@ const ScriptMenuList: React.FC<{ }); }} > -
-
+
{/* 判断菜单数量,再判断是否展开 */} - {(item.menus && item.menus?.length > systemConfig.menuExpandNum + {(item.menus.length > menuExpandNum ? expandMenuIndex[index] ? item.menus - : item.menus?.slice(0, systemConfig.menuExpandNum) + : item.menus?.slice(0, menuExpandNum) : item.menus )?.map((menu) => { if (menu.accessKey) { document.addEventListener("keypress", (e) => { if (e.key.toUpperCase() === menu.accessKey!.toUpperCase()) { - sendMenuAction(menu.sender, menu.channelFlag); + sendMenuAction(item.uuid, menu); } }); } @@ -271,7 +228,7 @@ const ScriptMenuList: React.FC<{ type="secondary" icon={} onClick={() => { - sendMenuAction(menu.sender, menu.channelFlag); + sendMenuAction(item.uuid, menu); }} > {menu.name} @@ -279,14 +236,12 @@ const ScriptMenuList: React.FC<{ ); })} - {item.menus && item.menus?.length > systemConfig.menuExpandNum && ( + {item.menus.length > menuExpandNum && (