From 933c91ebc09ab27e751035f93112c5c47854892f Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:31:19 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20Subscribe=20=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E4=BB=A3=E7=A0=81=E5=92=8C=E8=BF=BD=E5=8A=A0=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/repo/subscribe.ts | 25 ++-- src/app/service/service_worker/subscribe.ts | 136 +++++++++++++------- src/pages/install/App.tsx | 2 +- src/pages/options/routes/SubscribeList.tsx | 10 +- src/pkg/backup/struct.ts | 2 +- src/pkg/utils/script.ts | 24 +++- 6 files changed, 127 insertions(+), 72 deletions(-) diff --git a/src/app/repo/subscribe.ts b/src/app/repo/subscribe.ts index ff862fb8e..381ce1eb6 100644 --- a/src/app/repo/subscribe.ts +++ b/src/app/repo/subscribe.ts @@ -3,23 +3,28 @@ import type { SCMetadata } from "./metadata"; export { SCMetadata }; -export type SUBSCRIBE_STATUS = 1 | 2 | 3 | 4; -export const SUBSCRIBE_STATUS_ENABLE: SUBSCRIBE_STATUS = 1; -export const SUBSCRIBE_STATUS_DISABLE: SUBSCRIBE_STATUS = 2; +export const SubscribeStatusType = { + enable: 1, // 启动 checkSubscribeUpdate + disable: 2, // 停用 checkSubscribeUpdate + unknown3: 3, // 3 是什么? + unknown4: 4, // 4 是什么? +} as const; + +export type SubscribeStatusType = ValueOf; export interface SubscribeScript { uuid: string; - url: string; + url: string; // url of the user.js } export interface Subscribe { - url: string; + url: string; // url of the user.sub.js; 作为唯一键。暂时只支持网址。( 如需要支持 手动生成 Subscribe,日后可升级成 url / uuid ) name: string; - code: string; + code: string; // (meta) code of the user.sub.js author: string; - scripts: { [key: string]: SubscribeScript }; + scripts: Record; // 这里只储存脚本的 uuid 和 url 等资讯,而不是实际的代码 metadata: SCMetadata; - status: SUBSCRIBE_STATUS; + status: SubscribeStatusType; // 表示启动或停用。 3 和 4 不详 createtime: number; updatetime?: number; checktime: number; @@ -30,10 +35,6 @@ export class SubscribeDAO extends Repo { super("subscribe"); } - public findByUrl(url: string) { - return this.get(url); - } - public save(val: Subscribe) { return super._save(val.url, val); } diff --git a/src/app/service/service_worker/subscribe.ts b/src/app/service/service_worker/subscribe.ts index 479e9b0c1..3a756766d 100644 --- a/src/app/service/service_worker/subscribe.ts +++ b/src/app/service/service_worker/subscribe.ts @@ -2,7 +2,7 @@ import LoggerCore from "@App/app/logger/core"; import Logger from "@App/app/logger/logger"; import { ScriptDAO } from "@App/app/repo/scripts"; import type { SCMetadata, Subscribe, SubscribeScript } from "@App/app/repo/subscribe"; -import { SUBSCRIBE_STATUS_DISABLE, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe"; +import { SubscribeStatusType, SubscribeDAO } from "@App/app/repo/subscribe"; import { type SystemConfig } from "@App/pkg/config/config"; import { type IMessageQueue } from "@Packages/message/message_queue"; import { type Group } from "@Packages/message/server"; @@ -32,13 +32,17 @@ export class SubscribeService { } async install(param: { subscribe: Subscribe }) { + // 1)由安装页呼叫,进行 user.sub.js 的安装 + // 2)静默更新启动状态下,Subscribe 列表自动更新 const logger = this.logger.with({ subscribeUrl: param.subscribe.url, name: param.subscribe.name, }); try { - await this.subscribeDAO.save(param.subscribe); + await this.subscribeDAO.save(param.subscribe); // 所谓的安装,仅储存脚本资源。 logger.info("upsert subscribe success"); + // 广播后才会根据 subscrbie.scripts 的 url 取得/更新脚本 + // 注:installSubscribe 的广播是自己和自己对话。(不等待回应) this.mq.publish("installSubscribe", { subscribe: param.subscribe, }); @@ -84,85 +88,116 @@ export class SubscribeService { } } - // 更新订阅的脚本 + // 更新订阅的脚本( installSubscribe ) + // 已订阅的脚本则根据 Script脚本 本身的更新逻辑更新,与 Subscribe脚本 的更新无关 async upsertScript(url: string) { const subscribe = await this.subscribeDAO.get(url); - if (!subscribe) return; + if (!subscribe || !subscribe.metadata.usersubscribe) return; // 有效的 Subscribe 必定有 usersubscribe 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); - for (const url of scriptUrl) { + const addedScripts: string[] = []; + const removedScripts: SubscribeScript[] = []; + const metaScriptUrlSet = new Set(subscribe.metadata.scripturl || []); // 订阅列表 + const subscribeScripts = new Set(Object.keys(subscribe.scripts)); // 已关联 uuid 的列表 + // 注:首次安装时, subscribeScripts 是空的。 + for (const url of metaScriptUrlSet) { // 不存在于已安装的脚本中, 则添加 - if (!scripts.includes(url)) { - addScript.push(url); + if (!subscribeScripts.has(url)) { + addedScripts.push(url); } } - for (const url of scripts) { + for (const url of subscribeScripts) { // 不存在于订阅的脚本中, 则删除 - if (!scriptUrl.includes(url)) { - removeScript.push(subscribe.scripts[url]); + if (!metaScriptUrlSet.has(url)) { + removedScripts.push(subscribe.scripts[url]); } } - const notification: string[][] = [[], []]; - const result: Promise[] = []; - // 添加脚本 - addScript.forEach((url) => { - result.push( + const addedScriptNames: string[] = []; + const removedScriptNames: string[] = []; + const promises: Promise[] = []; + // 添加脚本: 根据 订阅列表 的 Script脚本URLs 进行安装 + addedScripts.forEach((url) => { + promises.push( (async () => { - const script = await this.scriptService.installByUrl(url, "subscribe", subscribe.url); - subscribe.scripts[url] = { - url, - uuid: script.uuid, - }; - notification[0].push(i18nName(script)); - return true; + // 先找一下已安装的 scripts + const existingScript = await this.scriptDAO.find((_key, script) => { + return script.downloadUrl === url || script.origin === url; + }); + if (existingScript?.[0]) { + // 仅关联至 已安装脚本的 uuid + // 注:1)已安装的脚本可能是用户用直接下载方式安装 + // 2)已安装的脚本可能是用户用其他 Subscribe 安装 + // 这里的 existingScript 的 subscribeUrl 值不一定是这个 Subscribe 的 url + subscribe.scripts[url] = { + url, + uuid: existingScript[0].uuid, + }; + } else { + // 安装Script脚本 ( script.subscribeUrl 会指定为这个 Subscribe. 当移除 Subscribe 时会一并移除 ) + const script = await this.scriptService.installByUrl(url, "subscribe", subscribe.url); + const name = i18nName(script); + // 把Script脚本关联至Subscribe + subscribe.scripts[url] = { + url, + uuid: script.uuid, + }; + addedScriptNames.push(name); + } })().catch((e) => { logger.error("install script failed", Logger.E(e)); - return false; }) ); }); - // 删除脚本 - removeScript.forEach((item) => { + // 删除脚本: 根据 subscribeScripts 的 Script脚本UUIDs 进行反安装 + removedScripts.forEach((item) => { // 通过uuid查询脚本id - result.push( + promises.push( (async () => { - const script = await this.scriptDAO.findByUUID(item.uuid); + // 以 uuid 找出已安装的Script脚本资讯 + const script = await this.scriptDAO.get(item.uuid); + const url = item.url; if (script) { - notification[1].push(i18nName(script)); - // 删除脚本 - this.scriptService.deleteScript(script.uuid); + const name = i18nName(script); + // 如果不是以 此 Subscribe 安装的话则略过删除 ( 例如其他 Subscribe, 直接Script安装,本地安装,等等 ) + if (script.subscribeUrl === subscribe.url) { + delete subscribe.scripts[url]; + // 删除脚本 + await this.scriptService.deleteScript(script.uuid); + removedScriptNames.push(name); + } else { + logger.warn("Subscribe Update: skip deletion", { + scriptUUID: script.uuid, + scriptUrl: url, + scriptName: name, + }); + } } - return true; })().catch((e) => { logger.error("delete script failed", Logger.E(e)); - return false; }) ); }); - await Promise.allSettled(result); + await Promise.allSettled(promises); + // 把 subscribe.scripts 的新资讯储存到 subscribeDAO await this.subscribeDAO.update(subscribe.url, subscribe); InfoNotification( i18n.t("notification.subscribe_update", { subscribeName: subscribe.name }), i18n.t("notification.subscribe_update_desc", { - newScripts: notification[0].join(","), - deletedScripts: notification[1].join(","), + newScripts: addedScriptNames.join(","), + deletedScripts: removedScriptNames.join(","), }) ); - logger.info("subscribe update", { - install: notification[0], - update: notification[1], + logger.info("subscribe list update", { + installed: addedScriptNames, + deleted: removedScriptNames, }); return true; @@ -184,9 +219,9 @@ export class SubscribeService { }); try { if (delayFn) await delayFn(); - const code = await fetchScriptBody(url); - const metadata = parseMetadata(code); - if (!metadata) { + const code = await fetchScriptBody(url); // user.sub.js 的 代码 + const metadata = parseMetadata(code); // user.sub.js 的 metadata = 代码内容分析; metadata.usersubscribe 是 空阵列 + if (!metadata || !metadata.usersubscribe) { logger.error("parse metadata failed"); return false; } @@ -211,11 +246,17 @@ export class SubscribeService { } // 检查更新 + /** + * @param url Subscribe脚本 的 url + * @param source 系统自动检查: "system"; subscribeClient.checkUpdate(subscribe.url) 的时候: "user" + * @returns + */ async checkUpdate(url: string, source: InstallSource) { const subscribe = await this.subscribeDAO.get(url); if (!subscribe) { return false; } + // 先写入更新触发时间 await this.subscribeDAO.update(url, { checktime: Date.now() }); const logger = this.logger.with({ url: subscribe.url, @@ -254,6 +295,7 @@ export class SubscribeService { if (silenceUpdate) { try { const newSubscribe = await prepareSubscribeByCode(code, url); + // 由于 Subscribe 不会含有 @connect, 因此静默更新启动的话, Subscribe 列表本身总是自动更新。 if (checkSilenceUpdate(newSubscribe.oldSubscribe!.metadata, newSubscribe.subscribe.metadata)) { logger.info("silence update subscribe"); this.install({ @@ -273,7 +315,7 @@ export class SubscribeService { }); for (const subscribe of list) { - if (!checkDisable && subscribe.status === SUBSCRIBE_STATUS_ENABLE) { + if (!checkDisable && subscribe.status === SubscribeStatusType.enable) { continue; } this.checkUpdate(subscribe.url, "system"); @@ -290,7 +332,7 @@ export class SubscribeService { }); try { await this.subscribeDAO.update(param.url, { - status: param.enable ? SUBSCRIBE_STATUS_ENABLE : SUBSCRIBE_STATUS_DISABLE, + status: param.enable ? SubscribeStatusType.enable : SubscribeStatusType.disable, }); logger.info("enable subscribe success"); return true; diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index b01cd9ed0..97dd408a8 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -475,7 +475,7 @@ function App() { try { if (scriptInfo?.userSubscribe) { - await subscribeClient.install(upsertScript as Subscribe); + await subscribeClient.install(upsertScript as Subscribe); // 首次安装时,upsertScript 里的 scripts 为空物件 Message.success(t("subscribe_success")!); setBtnText(t("subscribe_success")!); } else { diff --git a/src/pages/options/routes/SubscribeList.tsx b/src/pages/options/routes/SubscribeList.tsx index 30d6ee0e5..2031656a4 100644 --- a/src/pages/options/routes/SubscribeList.tsx +++ b/src/pages/options/routes/SubscribeList.tsx @@ -12,7 +12,7 @@ import { Typography, } from "@arco-design/web-react"; import type { Subscribe } from "@App/app/repo/subscribe"; -import { SUBSCRIBE_STATUS_DISABLE, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe"; +import { SubscribeStatusType, SubscribeDAO } from "@App/app/repo/subscribe"; import type { ColumnProps } from "@arco-design/web-react/es/Table"; import { IconSearch, IconUserAdd } from "@arco-design/web-react/icon"; import { semTime } from "@App/pkg/utils/dayjs"; @@ -69,18 +69,18 @@ function SubscribeList() { filters: [ { text: t("enable"), - value: SUBSCRIBE_STATUS_ENABLE, + value: SubscribeStatusType.enable, }, { text: t("disable"), - value: SUBSCRIBE_STATUS_DISABLE, + value: SubscribeStatusType.disable, }, ], onFilter: (value, row) => row.status === value, render: (col, item: ListType, index) => { return ( { @@ -98,7 +98,7 @@ function SubscribeList() { setListEntry(index, { loading: false, ...(statusChange && { - status: checked ? SUBSCRIBE_STATUS_ENABLE : SUBSCRIBE_STATUS_DISABLE, + status: checked ? SubscribeStatusType.enable : SubscribeStatusType.disable, }), }); }); diff --git a/src/pkg/backup/struct.ts b/src/pkg/backup/struct.ts index e05b8f50f..77c292ba2 100644 --- a/src/pkg/backup/struct.ts +++ b/src/pkg/backup/struct.ts @@ -101,7 +101,7 @@ export type SubscribeMeta = { export type SubscribeOptionsFile = { settings: { enabled: boolean }; - scripts: { [key: string]: SubscribeScript }; + scripts: Record; meta: SubscribeMeta; }; diff --git a/src/pkg/utils/script.ts b/src/pkg/utils/script.ts index ea9619593..a1ed4f77a 100644 --- a/src/pkg/utils/script.ts +++ b/src/pkg/utils/script.ts @@ -11,7 +11,7 @@ import { ScriptDAO, } from "@App/app/repo/scripts"; import type { Subscribe } from "@App/app/repo/subscribe"; -import { SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe"; +import { SubscribeStatusType, SubscribeDAO } from "@App/app/repo/subscribe"; import { nextTimeDisplay } from "./cron"; import { parseUserConfig } from "./yaml"; import { t as i18n_t } from "@App/locales/locales"; @@ -41,7 +41,7 @@ export function parseMetadata(code: string): SCMetadata | null { } if (!metadata.name || Object.keys(metadata).length < 3) return null; if (!metadata.namespace) metadata.namespace = [""]; - if (isSubscribe) metadata.usersubscribe = []; + if (isSubscribe) metadata.usersubscribe = []; // 如果是 user.sub.js, 在 metadata 会有一个额外的 usersubscribe return metadata; } @@ -71,7 +71,7 @@ export async function prepareScriptByCode( dao?: ScriptDAO, options?: { byEditor?: boolean; // 是否通过编辑器导入 - byWebRequest?: boolean; // 是否通过網頁連結安裝或更新 + byWebRequest?: boolean; // 是否通过网页连结安装或更新 } ): Promise<{ script: Script; oldScript?: Script; oldScriptCode?: string }> { dao = dao ?? new ScriptDAO(); @@ -219,6 +219,17 @@ export async function prepareSubscribeByCode( code: string, url: string ): Promise<{ subscribe: Subscribe; oldSubscribe?: Subscribe }> { + /* + // ==UserSubscribe== + // @name xxx + // @description 订阅xxx系列脚本 + // @version 0.1.0 + // @author You + // @connect www.baidu.com + // @scriptUrl https://script.tampermonkey.net.cn/48.user.js + // @scriptUrl https://script.tampermonkey.net.cn/49.user.js + // ==/UserSubscribe== + */ const dao = new SubscribeDAO(); const metadata = parseMetadata(code); if (!metadata) { @@ -229,20 +240,21 @@ export async function prepareSubscribeByCode( } const now = Date.now(); const subscribe: Subscribe = { - url, + url, // url of the user.sub.js name: metadata.name[0], code, author: (metadata.author && metadata.author[0]) || "", scripts: {}, metadata: metadata, - status: SUBSCRIBE_STATUS_ENABLE, + status: SubscribeStatusType.enable, createtime: now, updatetime: now, checktime: now, }; - const old = await dao.findByUrl(url); + const old = await dao.get(url); // 已存在 -> 把之前的 scripts, createtime, status 抽出来 if (old) { const { url, scripts, createtime, status } = old; + // url 是一样的;Subscribe 不使用 name 和 namespace 判断,仅使用 url 作唯一键 Object.assign(subscribe, { url, scripts, createtime, status }); } return { subscribe, oldSubscribe: old }; From 2e9c4b3c2eec638652119632100a43ce14cb48b7 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:57:19 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BD=9C=E8=80=85=E5=86=99=E5=8F=8D?= =?UTF-8?q?=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/subscribe.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/service_worker/subscribe.ts b/src/app/service/service_worker/subscribe.ts index 3a756766d..21810dbcf 100644 --- a/src/app/service/service_worker/subscribe.ts +++ b/src/app/service/service_worker/subscribe.ts @@ -315,7 +315,7 @@ export class SubscribeService { }); for (const subscribe of list) { - if (!checkDisable && subscribe.status === SubscribeStatusType.enable) { + if (!checkDisable && subscribe.status === SubscribeStatusType.disable) { continue; } this.checkUpdate(subscribe.url, "system");