From c4b47d117c3d5b484d1becd57796d08e2583d6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Fri, 3 Jan 2025 17:09:16 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=9A=E8=AE=AF=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/message/client.ts | 8 +- packages/message/message_queue.ts | 5 +- packages/message/window_message.ts | 99 ++++++++++ src/app/repo/scripts.ts | 9 +- src/app/service/offscreen/index.ts | 8 +- src/app/service/offscreen/script.ts | 20 ++ src/app/service/service_worker/client.ts | 24 ++- src/app/service/service_worker/script.ts | 59 +++++- src/pages/install/App.tsx | 2 +- src/pages/options/routes/ScriptList.tsx | 234 ++++++++++------------- src/pages/options/routes/utils.tsx | 2 +- src/sandbox.ts | 22 ++- src/store/features/script.ts | 96 ++++++++-- 13 files changed, 419 insertions(+), 169 deletions(-) create mode 100644 packages/message/window_message.ts create mode 100644 src/app/service/offscreen/script.ts diff --git a/packages/message/client.ts b/packages/message/client.ts index 238c2db..bfee9c4 100644 --- a/packages/message/client.ts +++ b/packages/message/client.ts @@ -1,6 +1,6 @@ -export function sendMessage(action: string, params?: any): Promise { +export function sendMessage(action: string, data?: any): Promise { return new Promise((resolve, reject) => { - chrome.runtime.sendMessage({ action, data: params }, (res) => { + chrome.runtime.sendMessage({ action, data }, (res) => { if (res.code) { console.error(res); reject(res.message); @@ -11,10 +11,10 @@ export function sendMessage(action: string, params?: any): Promise { }); } -export function connect(action: string, params?: any): Promise { +export function connect(action: string, data?: any): Promise { return new Promise((resolve) => { const port = chrome.runtime.connect(); - port.postMessage({ action, data: params }); + port.postMessage({ action, data }); resolve(port); }); } diff --git a/packages/message/message_queue.ts b/packages/message/message_queue.ts index b023310..7be383a 100644 --- a/packages/message/message_queue.ts +++ b/packages/message/message_queue.ts @@ -2,17 +2,20 @@ import EventEmitter from "eventemitter3"; import { connect } from "./client"; import { ApiFunction, Server } from "./server"; +export type SubscribeCallback = (message: any) => void; + export class Broker { constructor() {} // 订阅 - async subscribe(topic: string, handler: (message: any) => void) { + async subscribe(topic: string, handler: SubscribeCallback): Promise { const con = await connect("messageQueue", { action: "subscribe", topic }); con.onMessage.addListener((msg: { action: string; topic: string; message: any }) => { if (msg.action === "message") { handler(msg.message); } }); + return con; } // 发布 diff --git a/packages/message/window_message.ts b/packages/message/window_message.ts new file mode 100644 index 0000000..4f57c82 --- /dev/null +++ b/packages/message/window_message.ts @@ -0,0 +1,99 @@ +import { v4 as uuidv4 } from "uuid"; + +// 通过 window.postMessage/onmessage 实现通信 + +import EventEmitter from "eventemitter3"; + +// 消息体 +export type WindowMessageBody = { + messageId: string; // 消息id + type: "sendMessage" | "respMessage" | "connect"; // 消息类型 + data: any; // 消息数据 +}; + +export class WindowMessage { + EE: EventEmitter = new EventEmitter(); + + // source: Window 消息来源 + // target: Window 消息目标 + constructor( + private source: Window, + private target: Window + ) { + // 监听消息 + this.source.addEventListener("message", (e) => { + if (e.source === this.target) { + this.messageHandle(e.data); + } + }); + } + + messageHandle(data: WindowMessageBody) { + // 处理消息 + if (data.type === "sendMessage") { + // 接收到消息 + this.EE.emit("message", data.data, (resp: any) => { + // 发送响应消息 + const body: WindowMessageBody = { + messageId: data.messageId, + type: "respMessage", + data: resp, + }; + this.target.postMessage(body, "*"); + }); + } else if (data.type === "respMessage") { + // 接收到响应消息 + this.EE.emit("response:" + data.messageId, data); + } else if (data.type === "connect") { + this.EE.emit("connect", data.data, new WindowMessageConnect(data.messageId, this.EE, this.target)); + } else if (data.type === "disconnect") { + this.EE.emit("disconnect", data.data, new WindowMessageConnect(data.messageId, this.EE, this.target)); + } else if (data.type === "connectMessage") { + this.EE.emit("connectMessage", data.data, new WindowMessageConnect(data.messageId, this.EE, this.target)); + } + } + + onConnect(callback: (data: any, con: WindowMessageConnect) => void) { + this.EE.addListener("connect", callback); + } + + connect(action: string, data?: any): Promise { + return new Promise((resolve) => { + const body: WindowMessageBody = { + messageId: uuidv4(), + type: "connect", + data: { action, data }, + }; + this.target.postMessage(body, "*"); + resolve(new WindowMessageConnect(body.messageId, this.EE, this.target)); + }); + } + + onMessage(callback: (data: any, sendResponse: (data: any) => void) => void) { + this.EE.addListener("message", callback); + } + + sendMessage(action: string, data?: any): Promise { + return new Promise((resolve) => { + const body: WindowMessageBody = { + messageId: uuidv4(), + type: "sendMessage", + data: { action, data }, + }; + const callback = (body: WindowMessageBody) => { + this.EE.removeListener("response:" + body.messageId, callback); + resolve(body.data); + }; + this.EE.addListener("response:" + body.messageId, callback); + this.target.postMessage(body, "*"); + }); + } +} + +export class WindowMessageConnect { + constructor( + private messageId: string, + private EE: EventEmitter, + private target: Window + ) {} +} diff --git a/src/app/repo/scripts.ts b/src/app/repo/scripts.ts index 383cd59..a63d6ab 100644 --- a/src/app/repo/scripts.ts +++ b/src/app/repo/scripts.ts @@ -9,20 +9,15 @@ export const SCRIPT_TYPE_NORMAL: SCRIPT_TYPE = 1; export const SCRIPT_TYPE_CRONTAB: SCRIPT_TYPE = 2; export const SCRIPT_TYPE_BACKGROUND: SCRIPT_TYPE = 3; -export type SCRIPT_STATUS = 1 | 2 | 3 | 4; +export type SCRIPT_STATUS = 1 | 2; export const SCRIPT_STATUS_ENABLE: SCRIPT_STATUS = 1; export const SCRIPT_STATUS_DISABLE: SCRIPT_STATUS = 2; -// 弃用 -export const SCRIPT_STATUS_ERROR: SCRIPT_STATUS = 3; -export const SCRIPT_STATUS_DELETE: SCRIPT_STATUS = 4; -export type SCRIPT_RUN_STATUS = "running" | "complete" | "error" | "retry"; +export type SCRIPT_RUN_STATUS = "running" | "complete" | "error"; export const SCRIPT_RUN_STATUS_RUNNING: SCRIPT_RUN_STATUS = "running"; export const SCRIPT_RUN_STATUS_COMPLETE: SCRIPT_RUN_STATUS = "complete"; export const SCRIPT_RUN_STATUS_ERROR: SCRIPT_RUN_STATUS = "error"; -// 弃用 -export const SCRIPT_RUN_STATUS_RETRY: SCRIPT_RUN_STATUS = "retry"; export type Metadata = { [key: string]: string[] }; diff --git a/src/app/service/offscreen/index.ts b/src/app/service/offscreen/index.ts index 10d8def..50a50b1 100644 --- a/src/app/service/offscreen/index.ts +++ b/src/app/service/offscreen/index.ts @@ -1,11 +1,17 @@ import { Server } from "@Packages/message/server"; +import { ScriptService } from "./script"; +import { MessageQueue } from "@Packages/message/message_queue"; // offscreen环境的管理器 export class OffscreenManager { private api: Server = new Server("offscreen"); + private mq: MessageQueue = new MessageQueue(this.api); + initManager() { // 监听消息 - + const group = this.api.group("serviceWorker"); + const script = new ScriptService(group.group("script"), this.mq); + script.init(); } } diff --git a/src/app/service/offscreen/script.ts b/src/app/service/offscreen/script.ts new file mode 100644 index 0000000..846772c --- /dev/null +++ b/src/app/service/offscreen/script.ts @@ -0,0 +1,20 @@ +import LoggerCore from "@App/app/logger/core"; +import Logger from "@App/app/logger/logger"; +import { MessageQueue } from "@Packages/message/message_queue"; +import { Group } from "@Packages/message/server"; + +export class ScriptService { + logger: Logger; + + constructor( + private group: Group, + private mq: MessageQueue + ) { + this.logger = LoggerCore.logger().with({ service: "script" }); + } + + init() { + // 初始化, 执行所有的后台脚本, 设置定时脚本计时器 + + } +} diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index 7eaecbb..c31e063 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -1,6 +1,7 @@ import { Script } from "@App/app/repo/scripts"; import { Client } from "@Packages/message/client"; import { InstallSource } from "."; +import { Broker } from "@Packages/message/message_queue"; export class ScriptClient extends Client { constructor() { @@ -12,7 +13,26 @@ export class ScriptClient extends Client { return this.do("getInstallInfo", uuid); } - installScript(script: Script, upsertBy: InstallSource = "user") { - return this.do("installScript", { script, upsertBy }); + install(script: Script, upsertBy: InstallSource = "user") { + return this.do("install", { script, upsertBy }); + } + + delete(uuid: string) { + return this.do("delete", uuid); + } + + enable(uuid: string, enable: boolean) { + return this.do("enable", { uuid, enable }); } } + +export function subscribeScriptInstall( + border: Broker, + callback: (message: { script: Script; update: boolean }) => void +) { + return border.subscribe("installScript", callback); +} + +export function subscribeScriptDelete(border: Broker, callback: (message: { uuid: string }) => void) { + return border.subscribe("deleteScript", callback); +} diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index a0fa90e..dd2756d 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -6,7 +6,14 @@ import LoggerCore from "@App/app/logger/core"; import Cache from "@App/app/cache"; import CacheKey from "@App/app/cache_key"; import { openInCurrentTab } from "@App/pkg/utils/utils"; -import { Script, ScriptDAO } from "@App/app/repo/scripts"; +import { + Script, + SCRIPT_RUN_STATUS_COMPLETE, + SCRIPT_RUN_STATUS_RUNNING, + SCRIPT_STATUS_DISABLE, + SCRIPT_STATUS_ENABLE, + ScriptDAO, +} from "@App/app/repo/scripts"; import { MessageQueue } from "@Packages/message/message_queue"; import { InstallSource } from "."; @@ -145,11 +152,13 @@ export class ScriptService { version: script.metadata.version[0], upsertBy, }); + let update = false; const dao = new ScriptDAO(); // 判断是否已经安装 const oldScript = await dao.findByUUID(script.uuid); if (oldScript) { // 执行更新逻辑 + update = true; script.selfMetadata = oldScript.selfMetadata; } return dao @@ -157,7 +166,7 @@ export class ScriptService { .then(() => { logger.info("install success"); // 广播一下 - this.mq.publish("installScript", script); + this.mq.publish("installScript", { script, update }); return {}; }) .catch((e) => { @@ -166,10 +175,54 @@ export class ScriptService { }); } + async deleteScript(uuid: string) { + const logger = this.logger.with({ uuid }); + const dao = new ScriptDAO(); + const script = await dao.findByUUID(uuid); + if (!script) { + logger.error("script not found"); + throw new Error("script not found"); + } + return dao + .delete(uuid) + .then(() => { + logger.info("delete success"); + this.mq.publish("deleteScript", { uuid }); + return {}; + }) + .catch((e) => { + logger.error("delete error", Logger.E(e)); + throw e; + }); + } + + async enableScript(param: { uuid: string; enable: boolean }) { + const logger = this.logger.with({ uuid: param.uuid, enable: param.enable }); + const dao = new ScriptDAO(); + const script = await dao.findByUUID(param.uuid); + if (!script) { + logger.error("script not found"); + throw new Error("script not found"); + } + return dao + .update(param.uuid, { status: param.enable ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE }) + .then(() => { + logger.info("enable success"); + this.mq.publish("enableScript", { uuid: param.uuid, enable: param.enable }); + return {}; + }) + .catch((e) => { + logger.error("enable error", Logger.E(e)); + throw e; + }); + } + init() { this.listenerScriptInstall(); this.group.on("getInstallInfo", this.getInstallInfo); - this.group.on("installScript", this.installScript.bind(this)); + this.group.on("install", this.installScript.bind(this)); + this.group.on("delete", this.deleteScript.bind(this)); + this.group.on("enable", this.enableScript.bind(this)); } } diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index bbe91b9..db78a92 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -246,7 +246,7 @@ function App() { return; } new ScriptClient() - .installScript(upsertScript as Script) + .install(upsertScript as Script) .then(() => { if (isUpdate) { Message.success(t("install.update_success")!); diff --git a/src/pages/options/routes/ScriptList.tsx b/src/pages/options/routes/ScriptList.tsx index f028795..7acc28a 100644 --- a/src/pages/options/routes/ScriptList.tsx +++ b/src/pages/options/routes/ScriptList.tsx @@ -68,10 +68,21 @@ import CloudScriptPlan from "@App/pages/components/CloudScriptPlan"; import { useTranslation } from "react-i18next"; import { nextTime, semTime } from "@App/pkg/utils/utils"; import { i18nName } from "@App/locales/locales"; -import { ListHomeRender, ScriptIcons } from "./utils"; +import { getValues, ListHomeRender, ScriptIcons } from "./utils"; import { useAppDispatch, useAppSelector } from "@App/store/hooks"; -import { fetchScriptList, selectScripts } from "@App/store/features/script"; +import { + deleteScript, + requestEnableScript, + fetchAndSortScriptList, + requestDeleteScript, + ScriptLoading, + selectScripts, + sortScript, + upsertScript, +} from "@App/store/features/script"; import { selectScriptListColumnWidth } from "@App/store/features/setting"; +import { Broker } from "@Packages/message/message_queue"; +import { subscribeScriptDelete, subscribeScriptInstall } from "@App/app/service/service_worker/client"; type ListType = Script & { loading?: boolean }; @@ -87,7 +98,7 @@ function ScriptList() { const scriptListColumnWidth = useAppSelector(selectScriptListColumnWidth); const inputRef = useRef(null); const navigate = useNavigate(); - const openUserConfig = parseInt(useSearchParams()[0].get("userConfig") || "", 10); + const openUserConfig = useSearchParams()[0].get("userConfig") || ""; const [showAction, setShowAction] = useState(false); const [action, setAction] = useState(""); const [select, setSelect] = useState([]); @@ -96,9 +107,24 @@ function ScriptList() { const { t } = useTranslation(); useEffect(() => { - dispatch(fetchScriptList()); + dispatch(fetchAndSortScriptList()); // 监听脚本安装/运行 - // Monitor script running status + const border = new Broker(); + const subCon: chrome.runtime.Port[] = []; + + subscribeScriptInstall(border, (message) => { + dispatch(upsertScript(message.script)); + }).then((con) => subCon.push(con)); + + subscribeScriptDelete(border, (message) => { + dispatch(deleteScript(message.uuid)); + }).then((con) => subCon.push(con)); + + return () => { + subCon.forEach((con) => { + con.disconnect(); + }); + }; // const channel = runtimeCtrl.watchRunStatus(); // channel.setHandler(([id, status]: any) => { // setScriptList((list) => { @@ -123,6 +149,9 @@ function ScriptList() { key: "#", sorter: (a, b) => a.sort - b.sort, render(col) { + if (col < 0) { + return "-"; + } return col + 1; }, }, @@ -146,34 +175,14 @@ function ScriptList() { }, ], onFilter: (value, row) => row.status === value, - render: (col, item: ListType) => { + render: (col, item: ScriptLoading) => { return ( { - // setScriptList((list) => { - // const index = list.findIndex((script) => script.id === item.id); - // list[index].loading = true; - // let p: Promise; - // if (checked) { - // p = scriptCtrl.enable(item.id).then(() => { - // list[index].status = SCRIPT_STATUS_ENABLE; - // }); - // } else { - // p = scriptCtrl.disable(item.id).then(() => { - // list[index].status = SCRIPT_STATUS_DISABLE; - // }); - // } - // p.catch((err) => { - // Message.error(err); - // }).finally(() => { - // list[index].loading = false; - // setScriptList([...list]); - // }); - // return list; - // }); + dispatch(requestEnableScript({ uuid: item.uuid, enable: checked })); }} /> ); @@ -466,7 +475,7 @@ function ScriptList() { dataIndex: "action", key: "action", width: 160, - render(col, item: Script) { + render(col, item: ScriptLoading) { return ( @@ -482,18 +491,13 @@ function ScriptList() { title={t("confirm_delete_script")} icon={} onOk={() => { - // setScriptList((list) => { - // return list.filter((i) => i.id !== item.id); - // }); - // scriptCtrl.delete(item.id).catch((e) => { - // Message.error(`${t("delete_failed")}: ${e}`); - // }); + dispatch(requestDeleteScript(item.uuid)); }} >