diff --git a/src/app/repo/repo.ts b/src/app/repo/repo.ts index 0570f8b..5a33716 100644 --- a/src/app/repo/repo.ts +++ b/src/app/repo/repo.ts @@ -9,7 +9,7 @@ export abstract class Repo { return this.prefix + key; } - protected async _save(key: string, val: T):Promise { + protected async _save(key: string, val: T): Promise { return new Promise((resolve) => { const data = { [this.joinKey(key)]: val, diff --git a/src/app/repo/subscribe.ts b/src/app/repo/subscribe.ts index 349b497..6c91e9a 100644 --- a/src/app/repo/subscribe.ts +++ b/src/app/repo/subscribe.ts @@ -12,7 +12,6 @@ export interface SubscribeScript { } export interface Subscribe { - id: number; url: string; name: string; code: string; @@ -31,6 +30,10 @@ export class SubscribeDAO extends Repo { } public findByUrl(url: string) { - return this.findOne((key, value) => value.url === url); + return this.get(url); + } + + public save(val: Subscribe) { + return super._save(val.url, val); } } diff --git a/src/app/service/queue.ts b/src/app/service/queue.ts index d11e7d2..85f3993 100644 --- a/src/app/service/queue.ts +++ b/src/app/service/queue.ts @@ -1,6 +1,7 @@ import { MessageQueue } from "@Packages/message/message_queue"; import { Script, SCRIPT_RUN_STATUS } from "../repo/scripts"; import { InstallSource } from "./service_worker"; +import { Subscribe } from "../repo/subscribe"; export function subscribeScriptInstall( messageQueue: MessageQueue, @@ -13,6 +14,17 @@ export function subscribeScriptDelete(messageQueue: MessageQueue, callback: (mes return messageQueue.subscribe("deleteScript", callback); } +export function subscribeSubscribeInstall( + messageQueue: MessageQueue, + callback: (message: { subscribe: Subscribe; update: boolean }) => void +) { + return messageQueue.subscribe("installSubscribe", callback); +} + +export function publishSubscribeInstall(messageQueue: MessageQueue, message: { subscribe: Subscribe }) { + return messageQueue.publish("installSubscribe", message); +} + export type ScriptEnableCallbackValue = { uuid: string; enable: boolean }; export function subscribeScriptEnable( diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index 157a190..4ca41ff 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -9,6 +9,7 @@ import { FileSystemType } from "@Packages/filesystem/factory"; import { v4 as uuidv4 } from "uuid"; import Cache from "@App/app/cache"; import CacheKey from "@App/app/cache_key"; +import { Subscribe } from "@App/app/repo/subscribe"; export class ServiceWorkerClient extends Client { constructor(msg: MessageSend) { @@ -185,3 +186,25 @@ export class SynchronizeClient extends Client { }); } } + +export class SubscribeClient extends Client { + constructor(msg: MessageSend) { + super(msg, "serviceWorker/subscribe"); + } + + install(subscribe: Subscribe) { + return this.do("install", { subscribe }); + } + + delete(url: string) { + return this.do("delete", { url }); + } + + checkUpdate(url: string) { + return this.do("checkUpdate", { url }); + } + + enable(url: string, enable: boolean) { + return this.do("enable", { url, enable }); + } +} diff --git a/src/app/service/service_worker/index.ts b/src/app/service/service_worker/index.ts index 70e5d41..d6903af 100644 --- a/src/app/service/service_worker/index.ts +++ b/src/app/service/service_worker/index.ts @@ -8,6 +8,7 @@ import { ServiceWorkerMessageSend } from "@Packages/message/window_message"; import { PopupService } from "./popup"; import { SystemConfig } from "@App/pkg/config/config"; import { SynchronizeService } from "./synchronize"; +import { SubscribeService } from "./subscribe"; export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode"; @@ -49,6 +50,8 @@ export default class ServiceWorkerManager { systemConfig ); synchronize.init(); + const subscribe = new SubscribeService(systemConfig, this.api.group("subscribe"), this.mq, script); + subscribe.init(); // 定时器处理 chrome.alarms.onAlarm.addListener((alarm) => { @@ -63,6 +66,10 @@ export default class ServiceWorkerManager { synchronize.syncOnce(fs); }); }); + break; + case "checkSubscribeUpdate": + subscribe.checkSubscribeUpdate(); + break; } }); diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index c534804..e31d474 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -59,7 +59,7 @@ export class ScriptService { // 读取脚本url内容, 进行安装 const logger = this.logger.with({ url: targetUrl }); logger.debug("install script"); - this.openInstallPageByUrl(targetUrl).catch((e) => { + this.openInstallPageByUrl(targetUrl, "user").catch((e) => { logger.error("install script error", Logger.E(e)); // 如果打开失败, 则重定向到安装页 chrome.scripting.executeScript({ @@ -135,9 +135,9 @@ export class ScriptService { ); } - public openInstallPageByUrl(url: string) { + public openInstallPageByUrl(url: string, source: InstallSource) { const uuid = uuidv4(); - return fetchScriptInfo(url, "user", false, uuidv4()).then((info) => { + return fetchScriptInfo(url, source, false, uuidv4()).then((info) => { Cache.getInstance().set(CacheKey.scriptInstallInfo(uuid), info); setTimeout(() => { // 清理缓存 @@ -147,6 +147,19 @@ export class ScriptService { }); } + // 直接通过url静默安装脚本 + async installByUrl(url: string, source: InstallSource, subscribeUrl?: string) { + const info = await fetchScriptInfo(url, source, false, uuidv4()); + const prepareScript = await prepareScriptByCode(info.code, url, info.uuid); + prepareScript.script.subscribeUrl = subscribeUrl; + this.installScript({ + script: prepareScript.script, + code: info.code, + upsertBy: source, + }); + return Promise.resolve(prepareScript.script); + } + // 获取安装信息 getInstallInfo(uuid: string) { return Cache.getInstance().get(CacheKey.scriptInstallInfo(uuid)); @@ -330,7 +343,7 @@ export class ScriptService { } const newVersion = metadata.version && metadata.version[0]; if (!newVersion) { - logger.error("parse version failed", { version: "" }); + logger.error("parse version failed", { version: metadata.version }); return Promise.resolve(false); } let oldVersion = script.metadata.version && script.metadata.version[0]; @@ -393,16 +406,16 @@ export class ScriptService { }); } - checkScriptUpdate() { + async checkScriptUpdate() { + const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle(); + if (!checkCycle) { + return; + } this.scriptDAO.all().then(async (scripts) => { - const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle(); - if (!checkCycle) { - return; - } - const check = await this.systemConfig.getUpdateDisableScript(); + const checkDisableScript = await this.systemConfig.getUpdateDisableScript(); scripts.forEach(async (script) => { // 是否检查禁用脚本 - if (!check && script.status === SCRIPT_STATUS_DISABLE) { + if (!checkDisableScript && script.status === SCRIPT_STATUS_DISABLE) { return; } // 检查是否符合 diff --git a/src/app/service/service_worker/subscribe.ts b/src/app/service/service_worker/subscribe.ts new file mode 100644 index 0000000..48ec16b --- /dev/null +++ b/src/app/service/service_worker/subscribe.ts @@ -0,0 +1,280 @@ +import LoggerCore from "@App/app/logger/core"; +import Logger from "@App/app/logger/logger"; +import { ScriptDAO } from "@App/app/repo/scripts"; +import { + Subscribe, + SUBSCRIBE_STATUS_DISABLE, + SUBSCRIBE_STATUS_ENABLE, + SubscribeDAO, + SubscribeScript, +} from "@App/app/repo/subscribe"; +import { SystemConfig } from "@App/pkg/config/config"; +import { MessageQueue } from "@Packages/message/message_queue"; +import { Group } from "@Packages/message/server"; +import { InstallSource } from "."; +import { publishSubscribeInstall, subscribeSubscribeInstall } from "../queue"; +import { ScriptService } from "./script"; +import { checkSilenceUpdate, InfoNotification, ltever } from "@App/pkg/utils/utils"; +import { fetchScriptInfo, prepareSubscribeByCode, ScriptInfo } from "@App/pkg/utils/script"; +import Cache from "@App/app/cache"; +import CacheKey from "@App/app/cache_key"; + +export class SubscribeService { + logger: Logger; + subscribeDAO = new SubscribeDAO(); + scriptDAO = new ScriptDAO(); + + constructor( + private systemConfig: SystemConfig, + private group: Group, + private mq: MessageQueue, + private scriptService: ScriptService + ) { + this.logger = LoggerCore.logger().with({ service: "subscribe" }); + } + + async install(param: { subscribe: Subscribe }) { + const logger = this.logger.with({ + subscribeUrl: param.subscribe.url, + name: param.subscribe.name, + }); + try { + await this.subscribeDAO.save(param.subscribe); + logger.info("upsert subscribe success"); + publishSubscribeInstall(this.mq, { + subscribe: param.subscribe, + }); + return Promise.resolve(param.subscribe.url); + } catch (e) { + logger.error("upsert subscribe error", Logger.E(e)); + return Promise.reject(e); + } + } + + async delete(param: { url: string }) { + const logger = this.logger.with({ + subscribeUrl: param.url, + }); + const subscribe = await this.subscribeDAO.get(param.url); + if (!subscribe) { + logger.warn("subscribe not found"); + return Promise.resolve(false); + } + try { + // 删除相关脚本 + const scripts = await this.scriptDAO.find((_, value) => { + return value.subscribeUrl === param.url; + }); + scripts.forEach((script) => { + this.scriptService.deleteScript(script.uuid); + }); + // 删除订阅 + await this.subscribeDAO.delete(param.url); + logger.info("delete subscribe success"); + return Promise.resolve(true); + } catch (e) { + logger.error("uninstall subscribe error", Logger.E(e)); + return Promise.reject(e); + } + } + + // 更新订阅的脚本 + async upsertScript(subscribe: Subscribe) { + const logger = this.logger.with({ + url: subscribe.url, + name: subscribe.name, + }); + // 对比脚本是否有变化 + const addScript: string[] = []; + const removeScript: SubscribeScript[] = []; + const scriptUrl = subscribe.metadata.scripturl || []; + const scripts = Object.keys(subscribe.scripts); + scriptUrl.forEach((url) => { + // 不存在于已安装的脚本中, 则添加 + if (!scripts.includes(url)) { + addScript.push(url); + } + }); + scripts.forEach((url) => { + // 不存在于订阅的脚本中, 则删除 + if (!scriptUrl.includes(url)) { + removeScript.push(subscribe.scripts[url]); + } + }); + + const notification: string[][] = [[], []]; + const result: Promise[] = []; + // 添加脚本 + addScript.forEach((url) => { + result.push( + (async () => { + const script = await this.scriptService.installByUrl(url, "subscribe", subscribe.url); + subscribe.scripts[url] = { + url, + uuid: script.uuid, + }; + notification[0].push(script.name); + return Promise.resolve(true); + })().catch((e) => { + logger.error("install script failed", Logger.E(e)); + return Promise.resolve(false); + }) + ); + }); + // 删除脚本 + removeScript.forEach((item) => { + // 通过uuid查询脚本id + result.push( + (async () => { + const script = await this.scriptDAO.findByUUID(item.uuid); + if (script) { + notification[1].push(script.name); + // 删除脚本 + this.scriptService.deleteScript(script.uuid); + } + return Promise.resolve(true); + })().catch((e) => { + logger.error("delete script failed", Logger.E(e)); + return Promise.resolve(false); + }) + ); + }); + + await Promise.allSettled(result); + + await this.subscribeDAO.update(subscribe.url, subscribe); + + InfoNotification("订阅更新", `安装了:${notification[0].join(",")}\n删除了:${notification[1].join("\n")}`); + + logger.info("subscribe update", { + install: notification[0], + update: notification[1], + }); + + return Promise.resolve(true); + } + + // 检查更新 + async checkUpdate(url: string, source: InstallSource) { + const subscribe = await this.subscribeDAO.get(url); + if (!subscribe) { + return Promise.resolve(false); + } + const logger = this.logger.with({ + url: subscribe.url, + name: subscribe.name, + }); + await this.subscribeDAO.update(url, { checktime: new Date().getTime() }); + try { + const info = await fetchScriptInfo(subscribe.url, source, false, subscribe.url); + const { metadata } = info; + if (!metadata) { + logger.error("parse metadata failed"); + return Promise.resolve(false); + } + const newVersion = metadata.version && metadata.version[0]; + if (!newVersion) { + logger.error("parse version failed", { version: metadata.version }); + return Promise.resolve(false); + } + let oldVersion = subscribe.metadata.version && subscribe.metadata.version[0]; + if (!oldVersion) { + oldVersion = "0.0.0"; + } + // 对比版本大小 + if (ltever(newVersion, oldVersion, logger)) { + return Promise.resolve(false); + } + // 进行更新 + this.openUpdatePage(info); + return Promise.resolve(true); + } catch (e) { + logger.error("check update failed", Logger.E(e)); + return Promise.resolve(false); + } + } + + async openUpdatePage(info: ScriptInfo) { + const logger = this.logger.with({ + url: info.url, + }); + // 是否静默更新 + const silenceUpdate = await this.systemConfig.getSilenceUpdateScript(); + if (silenceUpdate) { + try { + const newSubscribe = await prepareSubscribeByCode(info.code, info.url); + if (checkSilenceUpdate(newSubscribe.oldSubscribe!.metadata, newSubscribe.subscribe.metadata)) { + logger.info("silence update subscribe"); + this.install({ + subscribe: newSubscribe.subscribe, + }); + return; + } + } catch (e) { + logger.error("prepare script failed", Logger.E(e)); + } + } + Cache.getInstance().set(CacheKey.scriptInstallInfo(info.uuid), info); + chrome.tabs.create({ + url: `/src/install.html?uuid=${info.uuid}`, + }); + } + + async checkSubscribeUpdate() { + const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle(); + if (!checkCycle) { + return; + } + this.logger.debug("start check update"); + const checkDisable = await this.systemConfig.getUpdateDisableScript(); + const list = await this.subscribeDAO.find((_, value) => { + return value.checktime + checkCycle * 1000 < Date.now(); + }); + + list.forEach((subscribe) => { + if (!checkDisable && subscribe.status === SUBSCRIBE_STATUS_ENABLE) { + return; + } + this.checkUpdate(subscribe.url, "system"); + }); + } + + requestCheckUpdate(url: string) { + return this.checkUpdate(url, "user"); + } + + enable(param: { url: string; enable: boolean }) { + const logger = this.logger.with({ + url: param.url, + }); + return this.subscribeDAO + .update(param.url, { + status: param.enable ? SUBSCRIBE_STATUS_ENABLE : SUBSCRIBE_STATUS_DISABLE, + }) + .then(() => { + logger.info("enable subscribe success"); + return Promise.resolve(true); + }) + .catch((e) => { + logger.error("enable subscribe error", Logger.E(e)); + return Promise.reject(e); + }); + } + + init() { + this.group.on("install", this.install.bind(this)); + this.group.on("delete", this.delete.bind(this)); + this.group.on("checkUpdate", this.requestCheckUpdate.bind(this)); + this.group.on("enable", this.enable.bind(this)); + + subscribeSubscribeInstall(this.mq, (message) => { + this.upsertScript(message.subscribe); + }); + + // 定时检查更新, 每10分钟检查一次 + chrome.alarms.create("checkSubscribeUpdate", { + delayInMinutes: 10, + periodInMinutes: 10, + }); + } +} diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 683387e..695dc57 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -7,7 +7,7 @@ import { i18nDescription, i18nName } from "@App/locales/locales"; import { useTranslation } from "react-i18next"; import { prepareScriptByCode, prepareSubscribeByCode, ScriptInfo } from "@App/pkg/utils/script"; import { nextTime } from "@App/pkg/utils/utils"; -import { scriptClient } from "../store/features/script"; +import { scriptClient, subscribeClient } from "../store/features/script"; type Permission = { label: string; color?: string; value: string[] }[]; @@ -253,18 +253,18 @@ function App() { return; } if (scriptInfo?.userSubscribe) { - // subscribeCtrl - // .upsert(upsertScript as Subscribe) - // .then(() => { - // Message.success(t("subscribe_success")!); - // setBtnText(t("subscribe_success")!); - // setTimeout(() => { - // closeWindow(); - // }, 200); - // }) - // .catch((e) => { - // Message.error(`${t("subscribe_failed")}: ${e}`); - // }); + subscribeClient + .install(upsertScript as Subscribe) + .then(() => { + Message.success(t("subscribe_success")!); + setBtnText(t("subscribe_success")!); + setTimeout(() => { + closeWindow(); + }, 500); + }) + .catch((e) => { + Message.error(`${t("subscribe_failed")}: ${e}`); + }); return; } scriptClient diff --git a/src/pages/options/routes/SubscribeList.tsx b/src/pages/options/routes/SubscribeList.tsx index bc16f86..c78e26c 100644 --- a/src/pages/options/routes/SubscribeList.tsx +++ b/src/pages/options/routes/SubscribeList.tsx @@ -1,47 +1,27 @@ import React, { useEffect, useRef, useState } from "react"; import Text from "@arco-design/web-react/es/Typography/text"; -import { - Button, - Card, - Input, - Message, - Popconfirm, - Switch, - Table, - Tag, - Tooltip, -} from "@arco-design/web-react"; -import { - Subscribe, - SUBSCRIBE_STATUS_DISABLE, - SUBSCRIBE_STATUS_ENABLE, - SubscribeDAO, -} from "@App/app/repo/subscribe"; +import { Button, Card, Input, Message, Popconfirm, Switch, Table, Tag, Tooltip } from "@arco-design/web-react"; +import { Subscribe, SUBSCRIBE_STATUS_DISABLE, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe"; import { ColumnProps } from "@arco-design/web-react/es/Table"; import { IconSearch, IconUserAdd } from "@arco-design/web-react/icon"; import { RefInputType } from "@arco-design/web-react/es/Input/interface"; import { semTime } from "@App/pkg/utils/utils"; import { RiDeleteBin5Fill } from "react-icons/ri"; import { useTranslation } from "react-i18next"; // 添加了 react-i18next 的引用 +import { subscribeClient } from "@App/pages/store/features/script"; type ListType = Subscribe & { loading?: boolean }; function SubscribeList() { const dao = new SubscribeDAO(); - const subscribeCtrl = IoC.instance( - SubscribeController - ) as SubscribeController; const [list, setList] = useState([]); const inputRef = useRef(null); const { t } = useTranslation(); // 使用 useTranslation hook useEffect(() => { - dao.table - .orderBy("id") - .toArray() - .then((subscribes) => { - setList(subscribes); - }); + dao.all().then((subscribes) => { + setList(subscribes); + }); }, []); const columns: ColumnProps[] = [ @@ -50,7 +30,13 @@ function SubscribeList() { dataIndex: "id", width: 70, key: "#", - sorter: (a, b) => a.id - b.id, + sorter: (a: Subscribe, b) => a.createtime - b.createtime, + render(col) { + if (col < 0) { + return "-"; + } + return col + 1; + }, }, { title: t("enable"), @@ -79,22 +65,18 @@ function SubscribeList() { onChange={(checked) => { list[index].loading = true; setList([...list]); - let p: Promise; - if (checked) { - p = subscribeCtrl.enable(item.id).then(() => { - list[index].status = SUBSCRIBE_STATUS_ENABLE; + subscribeClient + .enable(item.url, checked) + .then(() => { + list[index].status = checked ? SUBSCRIBE_STATUS_ENABLE : SUBSCRIBE_STATUS_DISABLE; + }) + .catch((err) => { + Message.error(err); + }) + .finally(() => { + list[index].loading = false; + setList([...list]); }); - } else { - p = subscribeCtrl.disable(item.id).then(() => { - list[index].status = SUBSCRIBE_STATUS_DISABLE; - }); - } - p.catch((err) => { - Message.error(err); - }).finally(() => { - list[index].loading = false; - setList([...list]); - }); }} /> ); @@ -169,14 +151,7 @@ function SubscribeList() { return
; } return (item.metadata.connect as string[]).map((val) => { - return ( - {val} - ); + return {val}; }); }, }, @@ -227,8 +202,8 @@ function SubscribeList() { id: "checkupdate", content: t("checking_for_updates"), }); - subscribeCtrl - .checkUpdate(subscribe.id) + subscribeClient + .checkUpdate(subscribe.url) .then((res) => { if (res) { Message.warning({ @@ -267,10 +242,15 @@ function SubscribeList() { title={t("confirm_delete_subscription")} icon={} onOk={() => { - setList(list.filter((val) => val.id !== item.id)); - subscribeCtrl.delete(item.id).catch((e) => { - Message.error(`${t("delete_failed")}: ${e}`); - }); + subscribeClient + .delete(item.url) + .then(() => { + setList(list.filter((val) => val.url !== item.url)); + Message.success(t("delete_success")); + }) + .catch((e) => { + Message.error(`${t("delete_failed")}: ${e}`); + }); }} >