From 319d8e25d8df26f7fb291cae480f0af7bc7ef750 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:23:35 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20Subscribe=20=E7=9A=84?= =?UTF-8?q?=20Script=20=E7=9A=84=20=E9=9D=99=E9=BB=98=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/script.ts | 9 ++++++++- src/pkg/utils/utils.ts | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 59873e093..29cb85aa5 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -48,6 +48,7 @@ import { getSimilarityScore, ScriptUpdateCheck } from "./script_update_check"; import { LocalStorageDAO } from "@App/app/repo/localStorage"; import { CompiledResourceDAO } from "@App/app/repo/resource"; import { initRegularUpdateCheck } from "./regular_updatecheck"; +import { SubscribeDAO } from "@App/app/repo/subscribe"; export type TCheckScriptUpdateOption = Partial< { checkType: "user"; noUpdateCheck?: number } | ({ checkType: "system" } & Record) @@ -58,6 +59,7 @@ export type TOpenBatchUpdatePageOption = { q: string; dontCheckNow: boolean }; export class ScriptService { logger: Logger; scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO(); + subscribeDAO: SubscribeDAO = new SubscribeDAO(); localStorageDAO: LocalStorageDAO = new LocalStorageDAO(); compiledResourceDAO: CompiledResourceDAO = new CompiledResourceDAO(); private readonly scriptUpdateCheck; @@ -841,7 +843,12 @@ export class ScriptService { if (update && (await this.systemConfig.getSilenceUpdateScript())) { try { const { oldScript, script } = await prepareScriptByCode(code, url, uuid); - if (checkSilenceUpdate(oldScript!.metadata, script.metadata)) { + let subscribeMetadata: SCMetadata | undefined; + if (oldScript?.subscribeUrl && oldScript.origin) { + const subscribe = await this.subscribeDAO.get(oldScript.subscribeUrl); + subscribeMetadata = subscribe?.metadata; + } + if (checkSilenceUpdate(oldScript!.metadata, script.metadata, subscribeMetadata)) { logger?.info("silence update script"); await this.installScript({ script, diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index 350913a6b..b2bc43ef7 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -154,12 +154,14 @@ export async function openInCurrentTab(url: string, tabId?: number) { } // 检查订阅规则是否改变,是否能够静默更新 -export function checkSilenceUpdate(oldMeta: SCMetadata, newMeta: SCMetadata): boolean { +export function checkSilenceUpdate(oldMeta: SCMetadata, newMeta: SCMetadata, subscribeMetadata?: SCMetadata | undefined): boolean { // 判断connect是否改变 const oldConnect = new Set(oldMeta.connect || []); const newConnect = new Set(newMeta.connect || []); + const subsConnect = subscribeMetadata ? new Set(subscribeMetadata?.connect || []) : null; // 老的里面没有新的就需要用户确认了 for (const key of newConnect) { + if (subsConnect?.has(key)) continue; // 如果有 Subscribe 而且它的 @connect 包含新的Script @connect, 则不用理会是否新增。 if (!oldConnect.has(key)) { return false; } From 630088ade61dbbc2be51ce0a5342c42ede9f65a3 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:32:27 +0900 Subject: [PATCH 2/6] lint --- src/pkg/utils/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index b2bc43ef7..96827599a 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -154,7 +154,11 @@ export async function openInCurrentTab(url: string, tabId?: number) { } // 检查订阅规则是否改变,是否能够静默更新 -export function checkSilenceUpdate(oldMeta: SCMetadata, newMeta: SCMetadata, subscribeMetadata?: SCMetadata | undefined): boolean { +export function checkSilenceUpdate( + oldMeta: SCMetadata, + newMeta: SCMetadata, + subscribeMetadata?: SCMetadata | undefined +): boolean { // 判断connect是否改变 const oldConnect = new Set(oldMeta.connect || []); const newConnect = new Set(newMeta.connect || []); From c27127438f2d9c5f5089024b7455d65da9a202f5 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 24 Feb 2026 10:41:53 +0900 Subject: [PATCH 3/6] =?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 | 137 +++++++++++++------- src/pages/install/App.tsx | 2 +- src/pages/options/routes/SubscribeList.tsx | 10 +- src/pkg/backup/struct.ts | 2 +- src/pkg/utils/script.ts | 22 +++- 6 files changed, 127 insertions(+), 71 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 5ec19875a..7baa859e9 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"; @@ -33,13 +33,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, }); @@ -85,85 +89,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; @@ -185,9 +220,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; } @@ -205,11 +240,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, @@ -248,6 +289,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({ @@ -267,7 +309,8 @@ export class SubscribeService { }); for (const subscribe of list) { - if (!checkDisable && subscribe.status === SUBSCRIBE_STATUS_ENABLE) { + if (!checkDisable && subscribe.status === SubscribeStatusType.disable) { + // 旧代码一直写反了这个 enable disable continue; } this.checkUpdate(subscribe.url, "system"); @@ -284,7 +327,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 6bbb48d51..f97354e6e 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -480,7 +480,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 d3674a3e6..651d47c48 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 76a5a5600..c07885d2c 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; } @@ -221,6 +221,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) { @@ -231,20 +242,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 cca2118ae0350573b1e96e3bca981c5e7555e08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Sun, 29 Mar 2026 15:14:29 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E6=AD=A3=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E8=84=9A=E6=9C=AC=E7=9A=84=E9=9D=99=E9=BB=98=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E4=B8=8E=20connect=20=E6=9D=83=E9=99=90=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根据文档设计,订阅脚本应始终静默更新,不受「非重要变更静默更新脚本」 开关控制;订阅下脚本的 connect 权限完全由订阅的 connect 覆盖。 - subscribe.ts: 订阅本身更新去掉 toggle 依赖,始终静默(除非 connect 变化) - script.ts: 订阅下脚本更新始终静默,不检查 toggle 和 connect - gm_api.ts: 运行时 GM_xmlhttpRequest/GM_cookie 的 connect 权限使用订阅声明的覆盖 - utils.ts: 简化 checkSilenceUpdate,移除不再需要的 subscribeMetadata 参数 --- .../service/service_worker/gm_api/gm_api.ts | 9 ++ .../gm_api/gm_api_subscribe.test.ts | 121 ++++++++++++++++++ src/app/service/service_worker/index.ts | 2 +- src/app/service/service_worker/script.ts | 31 ++--- src/app/service/service_worker/subscribe.ts | 28 ++-- src/pkg/utils/utils.ts | 14 +- 6 files changed, 161 insertions(+), 44 deletions(-) create mode 100644 src/app/service/service_worker/gm_api/gm_api_subscribe.test.ts diff --git a/src/app/service/service_worker/gm_api/gm_api.ts b/src/app/service/service_worker/gm_api/gm_api.ts index 1f853e284..aec8f90f1 100644 --- a/src/app/service/service_worker/gm_api/gm_api.ts +++ b/src/app/service/service_worker/gm_api/gm_api.ts @@ -1,6 +1,7 @@ import LoggerCore from "@App/app/logger/core"; import Logger from "@App/app/logger/logger"; import { ScriptDAO } from "@App/app/repo/scripts"; +import { SubscribeDAO } from "@App/app/repo/subscribe"; import { type IGetSender, type Group, GetSenderType } from "@Packages/message/server"; import type { ExtMessageSender, MessageSend, TMessageCommAction } from "@Packages/message/types"; import { connect, sendMessage } from "@Packages/message/client"; @@ -236,6 +237,7 @@ export default class GMApi { logger: Logger; scriptDAO: ScriptDAO = new ScriptDAO(); + subscribeDAO: SubscribeDAO = new SubscribeDAO(); constructor( private systemConfig: SystemConfig, @@ -273,6 +275,13 @@ export default class GMApi { if (!script) { throw new Error("script is not found"); } + // 订阅脚本的 connect 使用订阅声明的 connect 覆盖脚本自身的 + if (script.subscribeUrl) { + const subscribe = await this.subscribeDAO.get(script.subscribeUrl); + if (subscribe?.metadata?.connect) { + script.metadata = { ...script.metadata, connect: subscribe.metadata.connect }; + } + } return { ...data, script } as GMApiRequest; } diff --git a/src/app/service/service_worker/gm_api/gm_api_subscribe.test.ts b/src/app/service/service_worker/gm_api/gm_api_subscribe.test.ts new file mode 100644 index 000000000..07035c029 --- /dev/null +++ b/src/app/service/service_worker/gm_api/gm_api_subscribe.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { ScriptDAO, SCRIPT_TYPE_NORMAL, SCRIPT_STATUS_ENABLE, SCRIPT_RUN_STATUS_COMPLETE } from "@App/app/repo/scripts"; +import type { Script } from "@App/app/repo/scripts"; +import { SubscribeDAO, SUBSCRIBE_STATUS_ENABLE } from "@App/app/repo/subscribe"; +import type { Subscribe } from "@App/app/repo/subscribe"; +import type { SCMetadata } from "@App/app/repo/metadata"; +import GMApi, { MockGMExternalDependencies } from "./gm_api"; +import { initTestEnv } from "@Tests/utils"; +import { MockMessage } from "@Packages/message/mock_message"; +import { Server } from "@Packages/message/server"; +import EventEmitter from "eventemitter3"; +import { MessageQueue } from "@Packages/message/message_queue"; +import { SystemConfig } from "@App/pkg/config/config"; +import PermissionVerify from "../permission_verify"; + +initTestEnv(); + +function makeScript(uuid: string, connect?: string[], subscribeUrl?: string): Script { + const metadata: SCMetadata = {}; + if (connect) metadata.connect = connect; + return { + uuid, + name: "test-script", + namespace: "test", + metadata, + type: SCRIPT_TYPE_NORMAL, + status: SCRIPT_STATUS_ENABLE, + sort: 0, + runStatus: SCRIPT_RUN_STATUS_COMPLETE, + createtime: Date.now(), + checktime: Date.now(), + subscribeUrl, + }; +} + +function makeSubscribe(url: string, connect?: string[]): Subscribe { + const metadata: SCMetadata = {}; + if (connect) metadata.connect = connect; + return { + url, + name: "test-subscribe", + code: "", + author: "test", + scripts: {}, + metadata, + status: SUBSCRIBE_STATUS_ENABLE, + createtime: Date.now(), + checktime: Date.now(), + }; +} + +function createGMApi(): GMApi { + const ee = new EventEmitter(); + const message = new MockMessage(ee); + const messageQueue = new MessageQueue(); + const systemConfig = new SystemConfig(messageQueue); + const server = new Server("serviceWorker", message); + const permissionVerify = new PermissionVerify(server.group("permissionVerify"), messageQueue); + return new GMApi( + systemConfig, + permissionVerify, + server.group("runtime"), + message, + messageQueue, + {} as any, + new MockGMExternalDependencies() + ); +} + +describe("parseRequest 订阅脚本 connect 覆盖", () => { + let scriptDAO: ScriptDAO; + let subscribeDAO: SubscribeDAO; + let gmApi: GMApi; + + beforeEach(() => { + scriptDAO = new ScriptDAO(); + subscribeDAO = new SubscribeDAO(); + gmApi = createGMApi(); + }); + + it("订阅脚本的 connect 应被订阅的 connect 覆盖", async () => { + const subscribeUrl = "https://example.com/test.sub.js"; + const script = makeScript("uuid-1", ["script-domain.com"], subscribeUrl); + const subscribe = makeSubscribe(subscribeUrl, ["subscribe-domain.com", "api.example.com"]); + + await scriptDAO.save(script); + await subscribeDAO.save(subscribe); + + const req = await gmApi.parseRequest({ uuid: "uuid-1", api: "test", runFlag: "", params: [] }); + // connect 应该被订阅的覆盖,而不是脚本自身的 + expect(req.script.metadata.connect).toEqual(["subscribe-domain.com", "api.example.com"]); + }); + + it("普通脚本(无 subscribeUrl)的 connect 保持不变", async () => { + const script = makeScript("uuid-2", ["my-domain.com"]); + await scriptDAO.save(script); + + const req = await gmApi.parseRequest({ uuid: "uuid-2", api: "test", runFlag: "", params: [] }); + expect(req.script.metadata.connect).toEqual(["my-domain.com"]); + }); + + it("订阅不存在时脚本 connect 保持不变", async () => { + const script = makeScript("uuid-3", ["my-domain.com"], "https://gone.com/deleted.sub.js"); + await scriptDAO.save(script); + + const req = await gmApi.parseRequest({ uuid: "uuid-3", api: "test", runFlag: "", params: [] }); + expect(req.script.metadata.connect).toEqual(["my-domain.com"]); + }); + + it("订阅没有声明 connect 时脚本 connect 保持不变", async () => { + const subscribeUrl = "https://example.com/no-connect.sub.js"; + const script = makeScript("uuid-4", ["my-domain.com"], subscribeUrl); + const subscribe = makeSubscribe(subscribeUrl); // 无 connect + + await scriptDAO.save(script); + await subscribeDAO.save(subscribe); + + const req = await gmApi.parseRequest({ uuid: "uuid-4", api: "test", runFlag: "", params: [] }); + expect(req.script.metadata.connect).toEqual(["my-domain.com"]); + }); +}); diff --git a/src/app/service/service_worker/index.ts b/src/app/service/service_worker/index.ts index 51f381283..72ddd111d 100644 --- a/src/app/service/service_worker/index.ts +++ b/src/app/service/service_worker/index.ts @@ -90,7 +90,7 @@ export default class ServiceWorkerManager { scriptDAO ); synchronize.init(); - const subscribe = new SubscribeService(systemConfig, this.api.group("subscribe"), this.mq, script); + const subscribe = new SubscribeService(this.api.group("subscribe"), this.mq, script); subscribe.init(); const system = new SystemService( systemConfig, diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 00d1a6465..ca6b6da2b 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -47,7 +47,6 @@ import { getSimilarityScore, ScriptUpdateCheck } from "./script_update_check"; import { LocalStorageDAO } from "@App/app/repo/localStorage"; import { CompiledResourceDAO } from "@App/app/repo/resource"; import { initRegularUpdateCheck } from "./regular_updatecheck"; -import { SubscribeDAO } from "@App/app/repo/subscribe"; export type TCheckScriptUpdateOption = Partial< { checkType: "user"; noUpdateCheck?: number } | ({ checkType: "system" } & Record) @@ -71,7 +70,6 @@ export type TScriptInstallReturn = { export class ScriptService { logger: Logger; scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO(); - subscribeDAO: SubscribeDAO = new SubscribeDAO(); localStorageDAO: LocalStorageDAO = new LocalStorageDAO(); compiledResourceDAO: CompiledResourceDAO = new CompiledResourceDAO(); private readonly scriptUpdateCheck; @@ -886,25 +884,24 @@ export class ScriptService { ) { const upsertBy = options.source; const code = await fetchScriptBody(url); - if (update && (await this.systemConfig.getSilenceUpdateScript())) { + if (update) { try { const { oldScript, script } = await prepareScriptByCode(code, url, uuid); - let subscribeMetadata: SCMetadata | undefined; - if (oldScript?.subscribeUrl && oldScript.origin) { - const subscribe = await this.subscribeDAO.get(oldScript.subscribeUrl); - subscribeMetadata = subscribe?.metadata; - } - if (checkSilenceUpdate(oldScript!.metadata, script.metadata, subscribeMetadata)) { - logger?.info("silence update script"); - await this.installScript({ - script, - code, - upsertBy, - }); + // 订阅脚本始终静默更新,信任关系由订阅建立 + if (oldScript?.subscribeUrl) { + logger?.info("silence update subscribe script"); + await this.installScript({ script, code, upsertBy }); return 2; } - // 如果不符合静默更新规则,走后面的流程 - logger?.info("not silence update script, open install page"); + // 普通脚本:检查静默更新开关和 connect 变化 + if (await this.systemConfig.getSilenceUpdateScript()) { + if (checkSilenceUpdate(oldScript!.metadata, script.metadata)) { + logger?.info("silence update script"); + await this.installScript({ script, code, upsertBy }); + return 2; + } + logger?.info("not silence update script, open install page"); + } } catch (e) { logger?.error("prepare script failed", Logger.E(e)); } diff --git a/src/app/service/service_worker/subscribe.ts b/src/app/service/service_worker/subscribe.ts index 5ec19875a..7afb9df74 100644 --- a/src/app/service/service_worker/subscribe.ts +++ b/src/app/service/service_worker/subscribe.ts @@ -3,7 +3,6 @@ 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 { type SystemConfig } from "@App/pkg/config/config"; import { type IMessageQueue } from "@Packages/message/message_queue"; import { type Group } from "@Packages/message/server"; import { type ScriptService } from "./script"; @@ -24,7 +23,6 @@ export class SubscribeService { scriptDAO = new ScriptDAO(); constructor( - private systemConfig: SystemConfig, private group: Group, private mq: IMessageQueue, private scriptService: ScriptService @@ -239,25 +237,23 @@ export class SubscribeService { } } + // 订阅始终尝试静默更新,不受「非重要变更静默更新脚本」开关控制 + // 仅当订阅的 @connect 新增了域时才需要用户确认 async trySilenceUpdate(code: string, url: string) { const logger = this.logger.with({ url, }); - // 是否静默更新 - const silenceUpdate = await this.systemConfig.getSilenceUpdateScript(); - if (silenceUpdate) { - try { - const newSubscribe = await prepareSubscribeByCode(code, url); - if (checkSilenceUpdate(newSubscribe.oldSubscribe!.metadata, newSubscribe.subscribe.metadata)) { - logger.info("silence update subscribe"); - this.install({ - subscribe: newSubscribe.subscribe, - }); - return true; - } - } catch (e) { - logger.error("prepare script failed", Logger.E(e)); + try { + const newSubscribe = await prepareSubscribeByCode(code, url); + if (checkSilenceUpdate(newSubscribe.oldSubscribe!.metadata, newSubscribe.subscribe.metadata)) { + logger.info("silence update subscribe"); + this.install({ + subscribe: newSubscribe.subscribe, + }); + return true; } + } catch (e) { + logger.error("prepare script failed", Logger.E(e)); } } diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index d1d21acfb..7ba581ec7 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -144,19 +144,13 @@ export async function openInCurrentTab(url: string, tabId?: number) { } } -// 检查订阅规则是否改变,是否能够静默更新 -export function checkSilenceUpdate( - oldMeta: SCMetadata, - newMeta: SCMetadata, - subscribeMetadata?: SCMetadata | undefined -): boolean { - // 判断connect是否改变 +// 检查 connect 是否改变,是否能够静默更新 +// 用于订阅本身的更新检查和普通脚本的静默更新检查 +export function checkSilenceUpdate(oldMeta: SCMetadata, newMeta: SCMetadata): boolean { const oldConnect = new Set(oldMeta.connect || []); const newConnect = new Set(newMeta.connect || []); - const subsConnect = subscribeMetadata ? new Set(subscribeMetadata?.connect || []) : null; - // 老的里面没有新的就需要用户确认了 + // 新的 connect 中有老的没有的域,则需要用户确认 for (const key of newConnect) { - if (subsConnect?.has(key)) continue; // 如果有 Subscribe 而且它的 @connect 包含新的Script @connect, 则不用理会是否新增。 if (!oldConnect.has(key)) { return false; } From 77517bb4bafe35704c51b9f318ee4e5fa816c177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Sun, 29 Mar 2026 15:56:31 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D=20Subscri?= =?UTF-8?q?be=20=E8=84=9A=E6=9C=AC=E5=85=B3=E8=81=94=E6=B8=85=E7=90=86?= =?UTF-8?q?=E5=92=8C=E7=8A=B6=E6=80=81=E7=B1=BB=E5=9E=8B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复删除分支未清理 subscribe.scripts 关联导致每次更新重复触发的 bug - 删除未使用的 unknown3/unknown4 状态值 - 优化 upsertScript 中已安装脚本查找,用 Map 索引替代逐个全表扫描 --- src/app/repo/subscribe.ts | 8 +++---- src/app/service/service_worker/subscribe.ts | 25 ++++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/app/repo/subscribe.ts b/src/app/repo/subscribe.ts index 381ce1eb6..a77c5efef 100644 --- a/src/app/repo/subscribe.ts +++ b/src/app/repo/subscribe.ts @@ -4,10 +4,8 @@ import type { SCMetadata } from "./metadata"; export { SCMetadata }; export const SubscribeStatusType = { - enable: 1, // 启动 checkSubscribeUpdate - disable: 2, // 停用 checkSubscribeUpdate - unknown3: 3, // 3 是什么? - unknown4: 4, // 4 是什么? + enable: 1, + disable: 2, } as const; export type SubscribeStatusType = ValueOf; @@ -24,7 +22,7 @@ export interface Subscribe { author: string; scripts: Record; // 这里只储存脚本的 uuid 和 url 等资讯,而不是实际的代码 metadata: SCMetadata; - status: SubscribeStatusType; // 表示启动或停用。 3 和 4 不详 + status: SubscribeStatusType; createtime: number; updatetime?: number; checktime: number; diff --git a/src/app/service/service_worker/subscribe.ts b/src/app/service/service_worker/subscribe.ts index d708c513d..9a82e5b92 100644 --- a/src/app/service/service_worker/subscribe.ts +++ b/src/app/service/service_worker/subscribe.ts @@ -1,6 +1,6 @@ import LoggerCore from "@App/app/logger/core"; import Logger from "@App/app/logger/logger"; -import { ScriptDAO } from "@App/app/repo/scripts"; +import { ScriptDAO, type Script } from "@App/app/repo/scripts"; import type { SCMetadata, Subscribe, SubscribeScript } from "@App/app/repo/subscribe"; import { SubscribeDAO, SubscribeStatusType } from "@App/app/repo/subscribe"; import { type IMessageQueue } from "@Packages/message/message_queue"; @@ -115,6 +115,14 @@ export class SubscribeService { } } + // 一次性取出所有已安装脚本,建立 URL → Script 的索引,避免每个 addedScript 都全表扫描 + const allScripts = await this.scriptDAO.find(); + const scriptByUrl = new Map(); + for (const script of allScripts) { + if (script.downloadUrl) scriptByUrl.set(script.downloadUrl, script); + if (script.origin) scriptByUrl.set(script.origin, script); + } + const addedScriptNames: string[] = []; const removedScriptNames: string[] = []; const promises: Promise[] = []; @@ -122,18 +130,15 @@ export class SubscribeService { addedScripts.forEach((url) => { promises.push( (async () => { - // 先找一下已安装的 scripts - const existingScript = await this.scriptDAO.find((_key, script) => { - return script.downloadUrl === url || script.origin === url; - }); - if (existingScript?.[0]) { + const existingScript = scriptByUrl.get(url); + if (existingScript) { // 仅关联至 已安装脚本的 uuid // 注:1)已安装的脚本可能是用户用直接下载方式安装 // 2)已安装的脚本可能是用户用其他 Subscribe 安装 // 这里的 existingScript 的 subscribeUrl 值不一定是这个 Subscribe 的 url subscribe.scripts[url] = { url, - uuid: existingScript[0].uuid, + uuid: existingScript.uuid, }; } else { // 安装Script脚本 ( script.subscribeUrl 会指定为这个 Subscribe. 当移除 Subscribe 时会一并移除 ) @@ -159,12 +164,12 @@ export class SubscribeService { // 以 uuid 找出已安装的Script脚本资讯 const script = await this.scriptDAO.get(item.uuid); const url = item.url; + // 无论是否删除脚本,都需要清理 subscribe.scripts 中的关联 + delete subscribe.scripts[url]; if (script) { const name = i18nName(script); - // 如果不是以 此 Subscribe 安装的话则略过删除 ( 例如其他 Subscribe, 直接Script安装,本地安装,等等 ) + // 如果不是以此 Subscribe 安装的话则略过删除(例如其他 Subscribe、直接安装、本地安装等) if (script.subscribeUrl === subscribe.url) { - delete subscribe.scripts[url]; - // 删除脚本 await this.scriptService.deleteScript(script.uuid); removedScriptNames.push(name); } else { From 0569371f8968caa03fc2d4fbefec0761b59564ff Mon Sep 17 00:00:00 2001 From: wangyizhi Date: Sun, 29 Mar 2026 15:57:28 +0800 Subject: [PATCH 6/6] Update src/app/service/service_worker/subscribe.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- 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 9a82e5b92..3ed63b454 100644 --- a/src/app/service/service_worker/subscribe.ts +++ b/src/app/service/service_worker/subscribe.ts @@ -40,7 +40,7 @@ export class SubscribeService { try { await this.subscribeDAO.save(param.subscribe); // 所谓的安装,仅储存脚本资源。 logger.info("upsert subscribe success"); - // 广播后才会根据 subscrbie.scripts 的 url 取得/更新脚本 + // 广播后才会根据 subscribe.scripts 的 url 取得/更新脚本 // 注:installSubscribe 的广播是自己和自己对话。(不等待回应) this.mq.publish("installSubscribe", { subscribe: param.subscribe,