From 67b1b0d2a35815bd680652b6a99a1842e9fece5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 10 Jul 2025 20:34:19 +0800 Subject: [PATCH 01/32] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E9=9D=99=E6=80=81?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E4=BD=9C=E4=B8=BA=E5=AE=9E=E9=99=85=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/exec_script.test.ts | 2 +- src/app/service/content/gm_api.ts | 496 ++++++++++---------- 2 files changed, 241 insertions(+), 257 deletions(-) diff --git a/src/app/service/content/exec_script.test.ts b/src/app/service/content/exec_script.test.ts index bfa8a3eef..b9fc92350 100644 --- a/src/app/service/content/exec_script.test.ts +++ b/src/app/service/content/exec_script.test.ts @@ -5,7 +5,7 @@ import { ExtVersion } from "@App/app/const"; import { initTestEnv } from "@Tests/utils"; import { describe, expect, it } from "vitest"; import type { GMInfoEnv } from "./types"; -import { ScriptLoadInfo } from "../service_worker/types"; +import type { ScriptLoadInfo } from "../service_worker/types"; initTestEnv(); diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index 8d5845066..05b65f1f5 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -38,202 +38,10 @@ const GM_cookie = ( .catch((err) => { done && done(undefined, err); }); -} - -const GM_xmlhttpRequest = function (a: GMApi, details: GMTypes.XHRDetails) { - - const u = new URL(details.url, window.location.href); - if (details.headers) { - Object.keys(details.headers).forEach((key) => { - if (key.toLowerCase() === "cookie") { - details.cookie = details.headers![key]; - delete details.headers![key]; - } - }); - } - - const param: GMSend.XHRDetails = { - method: details.method, - timeout: details.timeout, - url: u.href, - headers: details.headers, - cookie: details.cookie, - context: details.context, - responseType: details.responseType, - overrideMimeType: details.overrideMimeType, - anonymous: details.anonymous, - user: details.user, - password: details.password, - redirect: details.redirect, - }; - if (!param.headers) { - param.headers = {}; - } - if (details.nocache) { - param.headers["Cache-Control"] = "no-cache"; - } - let connect: MessageConnect; - const handler = async () => { - // 处理数据 - if (details.data instanceof FormData) { - // 处理FormData - param.dataType = "FormData"; - const data: Array = []; - const keys: { [key: string]: boolean } = {}; - details.data.forEach((val, key) => { - keys[key] = true; - }); - // 处理FormData中的数据 - await Promise.all( - Object.keys(keys).map((key) => { - const values = (details.data).getAll(key); - return Promise.all( - values.map(async (val) => { - if (val instanceof File) { - const url = await a.CAT_createBlobUrl(val); - data.push({ - key, - type: "file", - val: url, - filename: val.name, - }); - } else { - data.push({ - key, - type: "text", - val, - }); - } - }) - ); - }) - ); - param.data = data; - } else if (details.data instanceof Blob) { - // 处理blob - param.dataType = "Blob"; - param.data = await a.CAT_createBlobUrl(details.data); - } else { - param.data = details.data; - } - - // 处理返回数据 - let readerStream: ReadableStream | undefined; - let controller: ReadableStreamDefaultController | undefined; - // 如果返回类型是arraybuffer或者blob的情况下,需要将返回的数据转化为blob - // 在background通过URL.createObjectURL转化为url,然后在content页读取url获取blob对象 - const responseType = details.responseType?.toLocaleLowerCase(); - const warpResponse = (old: (xhr: GMTypes.XHRResponse) => void) => { - if (responseType === "stream") { - readerStream = new ReadableStream({ - start(ctrl) { - controller = ctrl; - }, - }); - } - return async (xhr: GMTypes.XHRResponse) => { - if (xhr.response) { - if (responseType === "document") { - xhr.response = await a.CAT_fetchDocument(xhr.response); - xhr.responseXML = xhr.response; - xhr.responseType = "document"; - } else { - const resp = await a.CAT_fetchBlob(xhr.response); - if (responseType === "arraybuffer") { - xhr.response = await resp.arrayBuffer(); - } else { - xhr.response = resp; - } - } - } - if (responseType === "stream") { - xhr.response = readerStream; - } - old(xhr); - }; - }; - if ( - responseType === "arraybuffer" || - responseType === "blob" || - responseType === "document" || - responseType === "stream" - ) { - if (details.onload) { - details.onload = warpResponse(details.onload); - } - if (details.onreadystatechange) { - details.onreadystatechange = warpResponse(details.onreadystatechange); - } - if (details.onloadend) { - details.onloadend = warpResponse(details.onloadend); - } - // document类型读取blob,然后在content页转化为document对象 - if (responseType === "document") { - param.responseType = "blob"; - } - if (responseType === "stream") { - if (details.onloadstart) { - details.onloadstart = warpResponse(details.onloadstart); - } - } - } - - // 发送信息 - a.connect("GM_xmlhttpRequest", [param]).then((con) => { - connect = con; - con.onMessage((data: { action: string; data: any }) => { - // 处理返回 - switch (data.action) { - case "onload": - details.onload?.(data.data); - break; - case "onloadend": - details.onloadend?.(data.data); - break; - case "onloadstart": - details.onloadstart?.(data.data); - break; - case "onprogress": - details.onprogress?.(data.data); - break; - case "onreadystatechange": - details.onreadystatechange && details.onreadystatechange(data.data); - break; - case "ontimeout": - details.ontimeout?.(); - break; - case "onerror": - details.onerror?.(""); - break; - case "onabort": - details.onabort?.(); - break; - case "onstream": - controller?.enqueue(new Uint8Array(data.data)); - break; - default: - LoggerCore.logger().warn("GM_xmlhttpRequest resp is error", { - data, - }); - break; - } - }); - }); - }; - // 由于需要同步返回一个abort,但是一些操作是异步的,所以需要在这里处理 - handler(); - return { - abort: () => { - if (connect) { - connect.disconnect(); - } - }, - }; -} +}; // GM_Base 定义内部用变量和函数。均使用@protected export class GM_Base implements IGM_Base { - @GMContext.protected() protected runFlag!: string; @@ -261,17 +69,14 @@ export class GM_Base implements IGM_Base { @GMContext.protected() public eventId!: number; - constructor( - options: any = null, - obj: any = null - ) { - if(obj !== integrity) throw new TypeError("Illegal invocation"); + constructor(options: any = null, obj: any = null) { + if (obj !== integrity) throw new TypeError("Illegal invocation"); Object.assign(this, options); } @GMContext.protected() - static create(options: { [key:string]: any}){ - return (new GM_Base(options, integrity)) as GM_Base & { [key:string]: any }; + static create(options: { [key: string]: any }) { + return new GM_Base(options, integrity) as GM_Base & { [key: string]: any }; } // 单次回调使用 @@ -327,7 +132,7 @@ const GM_getValue = (a: GMApi, key: string, defaultValue?: any) => { return ret; } return defaultValue; -} +}; const GM_setValue = (a: GMApi, key: string, value: any) => { // 对object的value进行一次转化 @@ -341,7 +146,7 @@ const GM_setValue = (a: GMApi, key: string, value: any) => { a.scriptRes.value[key] = value; a.sendMessage("GM_setValue", [key, value]); } -} +}; // GMApi 定义 外部用API函数。不使用@protected export default class GMApi extends GM_Base { @@ -358,23 +163,26 @@ export default class GMApi extends GM_Base { // testing only const valueChangeListener = new Map(); const EE: EventEmitter = new EventEmitter(); - super({ - prefix, - message, - scriptRes, - valueChangeListener, - EE - }, integrity); + super( + { + prefix, + message, + scriptRes, + valueChangeListener, + EE, + }, + integrity + ); } // 获取脚本的值,可以通过@storageName让多个脚本共享一个储存空间 @GMContext.API() - public ['GM_getValue'](key: string, defaultValue?: any) { + public ["GM_getValue"](key: string, defaultValue?: any) { return GM_getValue(this, key, defaultValue); } @GMContext.API() - public ['GM.getValue'](key: string, defaultValue?: any): Promise { + public ["GM.getValue"](key: string, defaultValue?: any): Promise { // 兼容GM.getValue return new Promise((resolve) => { const ret = GM_getValue(this, key, defaultValue); @@ -383,12 +191,12 @@ export default class GMApi extends GM_Base { } @GMContext.API() - public ['GM_setValue'](key: string, value: any) { + public ["GM_setValue"](key: string, value: any) { GM_setValue(this, key, value); } @GMContext.API() - public ['GM.setValue'](key: string, value: any): Promise { + public ["GM.setValue"](key: string, value: any): Promise { // Asynchronous wrapper for GM_setValue to support GM.setValue return new Promise((resolve) => { GM_setValue(this, key, value); @@ -397,12 +205,12 @@ export default class GMApi extends GM_Base { } @GMContext.API() - public ['GM_deleteValue'](name: string): void { + public ["GM_deleteValue"](name: string): void { GM_setValue(this, name, undefined); } @GMContext.API() - public ['GM.deleteValue'](name: string): Promise { + public ["GM.deleteValue"](name: string): Promise { // Asynchronous wrapper for GM_deleteValue to support GM.deleteValue return new Promise((resolve) => { GM_setValue(this, name, undefined); @@ -411,12 +219,12 @@ export default class GMApi extends GM_Base { } @GMContext.API() - public ['GM_listValues'](): string[] { + public ["GM_listValues"](): string[] { return Object.keys(this.scriptRes.value); } @GMContext.API() - public ['GM.listValues'](): Promise { + public ["GM.listValues"](): Promise { // Asynchronous wrapper for GM_listValues to support GM.listValues return new Promise((resolve) => { const ret = Object.keys(this.scriptRes.value); @@ -425,7 +233,7 @@ export default class GMApi extends GM_Base { } @GMContext.API() - public ['GM_setValues'](values: { [key: string]: any }) { + public ["GM_setValues"](values: { [key: string]: any }) { if (values == null) { throw new Error("GM_setValues: values must not be null or undefined"); } @@ -439,7 +247,7 @@ export default class GMApi extends GM_Base { } @GMContext.API() - public ['GM_getValues'](keysOrDefaults: { [key: string]: any } | string[] | null | undefined) { + public ["GM_getValues"](keysOrDefaults: { [key: string]: any } | string[] | null | undefined) { if (keysOrDefaults == null) { // Returns all values return this.scriptRes.value; @@ -467,7 +275,7 @@ export default class GMApi extends GM_Base { // Asynchronous wrapper for GM.getValues @GMContext.API({ depend: ["GM_getValues"] }) - public ['GM.getValues']( + public ["GM.getValues"]( keysOrDefaults: { [key: string]: any } | string[] | null | undefined ): Promise<{ [key: string]: any }> { return new Promise((resolve) => { @@ -477,7 +285,7 @@ export default class GMApi extends GM_Base { } @GMContext.API() - public ['GM_deleteValues'](keys: string[]) { + public ["GM_deleteValues"](keys: string[]) { if (!Array.isArray(keys)) { console.warn(" GM_deleteValues: keys must be string[]"); return; @@ -489,7 +297,7 @@ export default class GMApi extends GM_Base { // Asynchronous wrapper for GM.deleteValues @GMContext.API({ depend: ["GM_deleteValues"] }) - public ['GM.deleteValues'](keys: string[]): Promise { + public ["GM.deleteValues"](keys: string[]): Promise { return new Promise((resolve) => { this.GM_deleteValues(keys); resolve(); @@ -541,11 +349,8 @@ export default class GMApi extends GM_Base { return (this.message).getAndDelRelatedTarget(data.relatedTarget) as Document; } - - @GMContext.API({ follow: 'GM.cookie' }) - ['GM.cookie.set']( - details: GMTypes.CookieDetails - ) { + @GMContext.API({ follow: "GM.cookie" }) + ["GM.cookie.set"](details: GMTypes.CookieDetails) { return new Promise((resolve, reject) => { GM_cookie(this, "set", details, (cookie, error) => { error ? reject(error) : resolve(cookie); @@ -553,12 +358,8 @@ export default class GMApi extends GM_Base { }); } - - - @GMContext.API({ follow: 'GM.cookie' }) - ['GM.cookie.list']( - details: GMTypes.CookieDetails - ) { + @GMContext.API({ follow: "GM.cookie" }) + ["GM.cookie.list"](details: GMTypes.CookieDetails) { return new Promise((resolve, reject) => { GM_cookie(this, "list", details, (cookie, error) => { error ? reject(error) : resolve(cookie); @@ -566,11 +367,8 @@ export default class GMApi extends GM_Base { }); } - - @GMContext.API({ follow: 'GM.cookie' }) - ['GM.cookie.delete']( - details: GMTypes.CookieDetails - ) { + @GMContext.API({ follow: "GM.cookie" }) + ["GM.cookie.delete"](details: GMTypes.CookieDetails) { return new Promise((resolve, reject) => { GM_cookie(this, "delete", details, (cookie, error) => { error ? reject(error) : resolve(cookie); @@ -578,41 +376,37 @@ export default class GMApi extends GM_Base { }); } - - @GMContext.API({ follow: 'GM_cookie' }) - ['GM_cookie.set']( + @GMContext.API({ follow: "GM_cookie" }) + ["GM_cookie.set"]( details: GMTypes.CookieDetails, done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void ) { GM_cookie(this, "set", details, done); } - - @GMContext.API({ follow: 'GM_cookie' }) - ['GM_cookie.list']( + @GMContext.API({ follow: "GM_cookie" }) + ["GM_cookie.list"]( details: GMTypes.CookieDetails, done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void ) { GM_cookie(this, "list", details, done); } - - @GMContext.API({ follow: 'GM_cookie' }) - ['GM_cookie.delete']( + @GMContext.API({ follow: "GM_cookie" }) + ["GM_cookie.delete"]( details: GMTypes.CookieDetails, done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void ) { GM_cookie(this, "delete", details, done); } - @GMContext.API() GM_cookie( action: string, details: GMTypes.CookieDetails, done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void ) { - GM_cookie(this, action, details, done); + GM_cookie(this, action, details, done); } @GMContext.API() @@ -783,14 +577,204 @@ export default class GMApi extends GM_Base { depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"], }) public GM_xmlhttpRequest(details: GMTypes.XHRDetails) { - return GM_xmlhttpRequest(this, details) + return GMApi.GM_xmlhttpRequest(this, details); } @GMContext.API({ depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"], }) - public ['GM.xmlHttpRequest'](details: GMTypes.XHRDetails) { - return GM_xmlhttpRequest(this, details) + public ["GM.xmlHttpRequest"](details: GMTypes.XHRDetails) { + return GMApi.GM_xmlhttpRequest(this, details); + } + + static GM_xmlhttpRequest(a: GMApi, details: GMTypes.XHRDetails) { + const u = new URL(details.url, window.location.href); + if (details.headers) { + Object.keys(details.headers).forEach((key) => { + if (key.toLowerCase() === "cookie") { + details.cookie = details.headers![key]; + delete details.headers![key]; + } + }); + } + + const param: GMSend.XHRDetails = { + method: details.method, + timeout: details.timeout, + url: u.href, + headers: details.headers, + cookie: details.cookie, + context: details.context, + responseType: details.responseType, + overrideMimeType: details.overrideMimeType, + anonymous: details.anonymous, + user: details.user, + password: details.password, + redirect: details.redirect, + }; + if (!param.headers) { + param.headers = {}; + } + if (details.nocache) { + param.headers["Cache-Control"] = "no-cache"; + } + let connect: MessageConnect; + const handler = async () => { + // 处理数据 + if (details.data instanceof FormData) { + // 处理FormData + param.dataType = "FormData"; + const data: Array = []; + const keys: { [key: string]: boolean } = {}; + details.data.forEach((val, key) => { + keys[key] = true; + }); + // 处理FormData中的数据 + await Promise.all( + Object.keys(keys).map((key) => { + const values = (details.data).getAll(key); + return Promise.all( + values.map(async (val) => { + if (val instanceof File) { + const url = await a.CAT_createBlobUrl(val); + data.push({ + key, + type: "file", + val: url, + filename: val.name, + }); + } else { + data.push({ + key, + type: "text", + val, + }); + } + }) + ); + }) + ); + param.data = data; + } else if (details.data instanceof Blob) { + // 处理blob + param.dataType = "Blob"; + param.data = await a.CAT_createBlobUrl(details.data); + } else { + param.data = details.data; + } + + // 处理返回数据 + let readerStream: ReadableStream | undefined; + let controller: ReadableStreamDefaultController | undefined; + // 如果返回类型是arraybuffer或者blob的情况下,需要将返回的数据转化为blob + // 在background通过URL.createObjectURL转化为url,然后在content页读取url获取blob对象 + const responseType = details.responseType?.toLocaleLowerCase(); + const warpResponse = (old: (xhr: GMTypes.XHRResponse) => void) => { + if (responseType === "stream") { + readerStream = new ReadableStream({ + start(ctrl) { + controller = ctrl; + }, + }); + } + return async (xhr: GMTypes.XHRResponse) => { + if (xhr.response) { + if (responseType === "document") { + xhr.response = await a.CAT_fetchDocument(xhr.response); + xhr.responseXML = xhr.response; + xhr.responseType = "document"; + } else { + const resp = await a.CAT_fetchBlob(xhr.response); + if (responseType === "arraybuffer") { + xhr.response = await resp.arrayBuffer(); + } else { + xhr.response = resp; + } + } + } + if (responseType === "stream") { + xhr.response = readerStream; + } + old(xhr); + }; + }; + if ( + responseType === "arraybuffer" || + responseType === "blob" || + responseType === "document" || + responseType === "stream" + ) { + if (details.onload) { + details.onload = warpResponse(details.onload); + } + if (details.onreadystatechange) { + details.onreadystatechange = warpResponse(details.onreadystatechange); + } + if (details.onloadend) { + details.onloadend = warpResponse(details.onloadend); + } + // document类型读取blob,然后在content页转化为document对象 + if (responseType === "document") { + param.responseType = "blob"; + } + if (responseType === "stream") { + if (details.onloadstart) { + details.onloadstart = warpResponse(details.onloadstart); + } + } + } + + // 发送信息 + a.connect("GM_xmlhttpRequest", [param]).then((con) => { + connect = con; + con.onMessage((data: { action: string; data: any }) => { + // 处理返回 + switch (data.action) { + case "onload": + details.onload?.(data.data); + break; + case "onloadend": + details.onloadend?.(data.data); + break; + case "onloadstart": + details.onloadstart?.(data.data); + break; + case "onprogress": + details.onprogress?.(data.data); + break; + case "onreadystatechange": + details.onreadystatechange && details.onreadystatechange(data.data); + break; + case "ontimeout": + details.ontimeout?.(); + break; + case "onerror": + details.onerror?.(""); + break; + case "onabort": + details.onabort?.(); + break; + case "onstream": + controller?.enqueue(new Uint8Array(data.data)); + break; + default: + LoggerCore.logger().warn("GM_xmlhttpRequest resp is error", { + data, + }); + break; + } + }); + }); + }; + // 由于需要同步返回一个abort,但是一些操作是异步的,所以需要在这里处理 + handler(); + return { + abort: () => { + if (connect) { + connect.disconnect(); + } + }, + }; } @GMContext.API() @@ -1090,7 +1074,7 @@ export default class GMApi extends GM_Base { } @GMContext.API({ depend: ["GM_getResourceText"] }) - public ['GM.getResourceText'](name: string): Promise { + public ["GM.getResourceText"](name: string): Promise { // Asynchronous wrapper for GM_getResourceText to support GM.getResourceText return new Promise((resolve) => { const ret = this.GM_getResourceText(name); @@ -1115,7 +1099,7 @@ export default class GMApi extends GM_Base { // GM_getResourceURL的异步版本,用来兼容GM.getResourceUrl @GMContext.API({ depend: ["GM_getResourceURL"] }) - public ['GM.getResourceUrl'](name: string, isBlobUrl?: boolean): Promise { + public ["GM.getResourceUrl"](name: string, isBlobUrl?: boolean): Promise { // Asynchronous wrapper for GM_getResourceURL to support GM.getResourceURL return new Promise((resolve) => { const ret = this.GM_getResourceURL(name, isBlobUrl); @@ -1124,12 +1108,12 @@ export default class GMApi extends GM_Base { } @GMContext.API() - ['window.close']() { + ["window.close"]() { return this.sendMessage("windowDotClose", []); } @GMContext.API() - ['window.focus']() { + ["window.focus"]() { return this.sendMessage("windowDotFocus", []); } } From dcdc36f19f3bcb61906ff1aa422fb5f6c2c049ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 10 Jul 2025 20:56:04 +0800 Subject: [PATCH 02/32] =?UTF-8?q?=E8=B0=83=E6=95=B4GMApi=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/gm_api.ts | 134 +++++++++++++++--------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index 05b65f1f5..4bacf831d 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -21,25 +21,6 @@ export interface IGM_Base { const integrity = {}; // 僅防止非法实例化 -const GM_cookie = ( - a: IGM_Base, - action: string, - details: GMTypes.CookieDetails, - done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void -) => { - // 如果url和域名都没有,自动填充当前url - if (!details.url && !details.domain) { - details.url = window.location.href; - } - a.sendMessage("GM_cookie", [action, details]) - .then((resp: any) => { - done && done(resp, undefined); - }) - .catch((err) => { - done && done(undefined, err); - }); -}; - // GM_Base 定义内部用变量和函数。均使用@protected export class GM_Base implements IGM_Base { @GMContext.protected() @@ -126,28 +107,6 @@ export class GM_Base implements IGM_Base { } } -const GM_getValue = (a: GMApi, key: string, defaultValue?: any) => { - const ret = a.scriptRes.value[key]; - if (ret !== undefined) { - return ret; - } - return defaultValue; -}; - -const GM_setValue = (a: GMApi, key: string, value: any) => { - // 对object的value进行一次转化 - if (typeof value === "object") { - value = JSON.parse(JSON.stringify(value)); - } - if (value === undefined) { - delete a.scriptRes.value[key]; - a.sendMessage("GM_setValue", [key]); - } else { - a.scriptRes.value[key] = value; - a.sendMessage("GM_setValue", [key, value]); - } -}; - // GMApi 定义 外部用API函数。不使用@protected export default class GMApi extends GM_Base { /** @@ -177,49 +136,71 @@ export default class GMApi extends GM_Base { // 获取脚本的值,可以通过@storageName让多个脚本共享一个储存空间 @GMContext.API() - public ["GM_getValue"](key: string, defaultValue?: any) { - return GM_getValue(this, key, defaultValue); + public GM_getValue(key: string, defaultValue?: any) { + return GMApi.GM_getValue(this, key, defaultValue); + } + + static GM_getValue(a: GMApi, key: string, defaultValue?: any) { + const ret = a.scriptRes.value[key]; + if (ret !== undefined) { + return ret; + } + return defaultValue; } @GMContext.API() public ["GM.getValue"](key: string, defaultValue?: any): Promise { // 兼容GM.getValue return new Promise((resolve) => { - const ret = GM_getValue(this, key, defaultValue); + const ret = GMApi.GM_getValue(this, key, defaultValue); resolve(ret); }); } @GMContext.API() - public ["GM_setValue"](key: string, value: any) { - GM_setValue(this, key, value); + public GM_setValue(key: string, value: any) { + GMApi.GM_setValue(this, key, value); + } + + static GM_setValue(a: GMApi, key: string, value: any) { + // 对object的value进行一次转化 + if (typeof value === "object") { + value = JSON.parse(JSON.stringify(value)); + } + if (value === undefined) { + delete a.scriptRes.value[key]; + a.sendMessage("GM_setValue", [key]); + } else { + a.scriptRes.value[key] = value; + a.sendMessage("GM_setValue", [key, value]); + } } @GMContext.API() public ["GM.setValue"](key: string, value: any): Promise { // Asynchronous wrapper for GM_setValue to support GM.setValue return new Promise((resolve) => { - GM_setValue(this, key, value); + GMApi.GM_setValue(this, key, value); resolve(); }); } @GMContext.API() - public ["GM_deleteValue"](name: string): void { - GM_setValue(this, name, undefined); + public GM_deleteValue(name: string): void { + GMApi.GM_setValue(this, name, undefined); } @GMContext.API() public ["GM.deleteValue"](name: string): Promise { // Asynchronous wrapper for GM_deleteValue to support GM.deleteValue return new Promise((resolve) => { - GM_setValue(this, name, undefined); + GMApi.GM_setValue(this, name, undefined); resolve(); }); } @GMContext.API() - public ["GM_listValues"](): string[] { + public GM_listValues(): string[] { return Object.keys(this.scriptRes.value); } @@ -233,7 +214,7 @@ export default class GMApi extends GM_Base { } @GMContext.API() - public ["GM_setValues"](values: { [key: string]: any }) { + public GM_setValues(values: { [key: string]: any }) { if (values == null) { throw new Error("GM_setValues: values must not be null or undefined"); } @@ -242,12 +223,12 @@ export default class GMApi extends GM_Base { } Object.keys(values).forEach((key) => { const value = values[key]; - GM_setValue(this, key, value); + GMApi.GM_setValue(this, key, value); }); } @GMContext.API() - public ["GM_getValues"](keysOrDefaults: { [key: string]: any } | string[] | null | undefined) { + public GM_getValues(keysOrDefaults: { [key: string]: any } | string[] | null | undefined) { if (keysOrDefaults == null) { // Returns all values return this.scriptRes.value; @@ -267,7 +248,7 @@ export default class GMApi extends GM_Base { // Handle object with default values (e.g., { foo: 1, bar: 2, baz: 3 }) Object.keys(keysOrDefaults).forEach((key) => { const defaultValue = keysOrDefaults[key]; - result[key] = GM_getValue(this, key, defaultValue); + result[key] = GMApi.GM_getValue(this, key, defaultValue); }); } return result; @@ -285,13 +266,13 @@ export default class GMApi extends GM_Base { } @GMContext.API() - public ["GM_deleteValues"](keys: string[]) { + public GM_deleteValues(keys: string[]) { if (!Array.isArray(keys)) { - console.warn(" GM_deleteValues: keys must be string[]"); + console.warn("GM_deleteValues: keys must be string[]"); return; } keys.forEach((key) => { - GM_setValue(this, key, undefined); + GMApi.GM_setValue(this, key, undefined); }); } @@ -306,8 +287,6 @@ export default class GMApi extends GM_Base { eventId: number = 0; - menuMap: Map | undefined; - @GMContext.API() public GM_addValueChangeListener(name: string, listener: GMTypes.ValueChangeListener): number { this.eventId += 1; @@ -352,7 +331,7 @@ export default class GMApi extends GM_Base { @GMContext.API({ follow: "GM.cookie" }) ["GM.cookie.set"](details: GMTypes.CookieDetails) { return new Promise((resolve, reject) => { - GM_cookie(this, "set", details, (cookie, error) => { + GMApi.GM_cookie(this, "set", details, (cookie, error) => { error ? reject(error) : resolve(cookie); }); }); @@ -361,7 +340,7 @@ export default class GMApi extends GM_Base { @GMContext.API({ follow: "GM.cookie" }) ["GM.cookie.list"](details: GMTypes.CookieDetails) { return new Promise((resolve, reject) => { - GM_cookie(this, "list", details, (cookie, error) => { + GMApi.GM_cookie(this, "list", details, (cookie, error) => { error ? reject(error) : resolve(cookie); }); }); @@ -370,7 +349,7 @@ export default class GMApi extends GM_Base { @GMContext.API({ follow: "GM.cookie" }) ["GM.cookie.delete"](details: GMTypes.CookieDetails) { return new Promise((resolve, reject) => { - GM_cookie(this, "delete", details, (cookie, error) => { + GMApi.GM_cookie(this, "delete", details, (cookie, error) => { error ? reject(error) : resolve(cookie); }); }); @@ -381,7 +360,7 @@ export default class GMApi extends GM_Base { details: GMTypes.CookieDetails, done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void ) { - GM_cookie(this, "set", details, done); + GMApi.GM_cookie(this, "set", details, done); } @GMContext.API({ follow: "GM_cookie" }) @@ -389,7 +368,7 @@ export default class GMApi extends GM_Base { details: GMTypes.CookieDetails, done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void ) { - GM_cookie(this, "list", details, done); + GMApi.GM_cookie(this, "list", details, done); } @GMContext.API({ follow: "GM_cookie" }) @@ -397,7 +376,7 @@ export default class GMApi extends GM_Base { details: GMTypes.CookieDetails, done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void ) { - GM_cookie(this, "delete", details, done); + GMApi.GM_cookie(this, "delete", details, done); } @GMContext.API() @@ -406,9 +385,30 @@ export default class GMApi extends GM_Base { details: GMTypes.CookieDetails, done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void ) { - GM_cookie(this, action, details, done); + GMApi.GM_cookie(this, action, details, done); } + static GM_cookie( + a: IGM_Base, + action: string, + details: GMTypes.CookieDetails, + done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void + ) { + // 如果url和域名都没有,自动填充当前url + if (!details.url && !details.domain) { + details.url = window.location.href; + } + a.sendMessage("GM_cookie", [action, details]) + .then((resp: any) => { + done && done(resp, undefined); + }) + .catch((err) => { + done && done(undefined, err); + }); + } + + menuMap: Map | undefined; + @GMContext.API() GM_registerMenuCommand( name: string, From a0ee0ceb9762811b9e349056c3a6af5db7eace00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 10 Jul 2025 21:26:21 +0800 Subject: [PATCH 03/32] =?UTF-8?q?=E5=AE=9E=E7=8E=B0alias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/gm_api.ts | 63 +++++++++++++-------------- src/app/service/content/gm_context.ts | 7 +++ src/app/service/content/types.ts | 1 + 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index 4bacf831d..425e3268e 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -265,6 +265,14 @@ export default class GMApi extends GM_Base { }); } + @GMContext.API({ depend: ["GM_setValues"] }) + public ["GM.setValues"](values: { [key: string]: any }): Promise { + return new Promise((resolve) => { + this.GM_setValues(values); + resolve(); + }); + } + @GMContext.API() public GM_deleteValues(keys: string[]) { if (!Array.isArray(keys)) { @@ -287,19 +295,19 @@ export default class GMApi extends GM_Base { eventId: number = 0; - @GMContext.API() + @GMContext.API({ alias: "GM.addValueChangeListener" }) public GM_addValueChangeListener(name: string, listener: GMTypes.ValueChangeListener): number { this.eventId += 1; this.valueChangeListener.set(this.eventId, { name, listener }); return this.eventId; } - @GMContext.API() + @GMContext.API({ alias: "GM.removeValueChangeListener" }) public GM_removeValueChangeListener(listenerId: number): void { this.valueChangeListener.delete(listenerId); } - @GMContext.API() + @GMContext.API({ alias: "GM.log" }) GM_log(message: string, level: GMTypes.LoggerLevel = "info", ...labels: GMTypes.LoggerLabel[]) { if (typeof message !== "string") { message = JSON.stringify(message); @@ -409,7 +417,7 @@ export default class GMApi extends GM_Base { menuMap: Map | undefined; - @GMContext.API() + @GMContext.API({ alias: "GM.registerMenuCommand" }) GM_registerMenuCommand( name: string, listener: (inputValue?: any) => void, @@ -456,7 +464,7 @@ export default class GMApi extends GM_Base { return this.GM_registerMenuCommand(...args); } - @GMContext.API() + @GMContext.API({ alias: "GM.addStyle" }) GM_addStyle(css: string) { // 与content页的消息通讯实际是同步,此方法不需要经过background // 这里直接使用同步的方式去处理, 不要有promise @@ -480,7 +488,7 @@ export default class GMApi extends GM_Base { return (this.message).getAndDelRelatedTarget(resp.data); } - @GMContext.API() + @GMContext.API({ alias: "GM.addElement" }) GM_addElement(parentNode: EventTarget | string, tagName: any, attrs?: any) { // 与content页的消息通讯实际是同步,此方法不需要经过background // 这里直接使用同步的方式去处理, 不要有promise @@ -509,7 +517,7 @@ export default class GMApi extends GM_Base { return (this.message).getAndDelRelatedTarget(resp.data); } - @GMContext.API() + @GMContext.API({ alias: "GM.unregisterMenuCommand" }) GM_unregisterMenuCommand(id: number): void { if (!this.menuMap) { this.menuMap = new Map(); @@ -574,20 +582,10 @@ export default class GMApi extends GM_Base { // 用于脚本跨域请求,需要@connect domain指定允许的域名 @GMContext.API({ + alias: "GM.xmlHttpRequest", depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"], }) public GM_xmlhttpRequest(details: GMTypes.XHRDetails) { - return GMApi.GM_xmlhttpRequest(this, details); - } - - @GMContext.API({ - depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"], - }) - public ["GM.xmlHttpRequest"](details: GMTypes.XHRDetails) { - return GMApi.GM_xmlhttpRequest(this, details); - } - - static GM_xmlhttpRequest(a: GMApi, details: GMTypes.XHRDetails) { const u = new URL(details.url, window.location.href); if (details.headers) { Object.keys(details.headers).forEach((key) => { @@ -636,7 +634,7 @@ export default class GMApi extends GM_Base { return Promise.all( values.map(async (val) => { if (val instanceof File) { - const url = await a.CAT_createBlobUrl(val); + const url = await this.CAT_createBlobUrl(val); data.push({ key, type: "file", @@ -658,7 +656,7 @@ export default class GMApi extends GM_Base { } else if (details.data instanceof Blob) { // 处理blob param.dataType = "Blob"; - param.data = await a.CAT_createBlobUrl(details.data); + param.data = await this.CAT_createBlobUrl(details.data); } else { param.data = details.data; } @@ -680,11 +678,11 @@ export default class GMApi extends GM_Base { return async (xhr: GMTypes.XHRResponse) => { if (xhr.response) { if (responseType === "document") { - xhr.response = await a.CAT_fetchDocument(xhr.response); + xhr.response = await this.CAT_fetchDocument(xhr.response); xhr.responseXML = xhr.response; xhr.responseType = "document"; } else { - const resp = await a.CAT_fetchBlob(xhr.response); + const resp = await this.CAT_fetchBlob(xhr.response); if (responseType === "arraybuffer") { xhr.response = await resp.arrayBuffer(); } else { @@ -725,7 +723,7 @@ export default class GMApi extends GM_Base { } // 发送信息 - a.connect("GM_xmlhttpRequest", [param]).then((con) => { + this.connect("GM_xmlhttpRequest", [param]).then((con) => { connect = con; con.onMessage((data: { action: string; data: any }) => { // 处理返回 @@ -777,7 +775,7 @@ export default class GMApi extends GM_Base { }; } - @GMContext.API() + @GMContext.API({ alias: "GM.download" }) GM_download(url: GMTypes.DownloadDetails | string, filename?: string): GMTypes.AbortHandle { let details: GMTypes.DownloadDetails; if (typeof url === "string") { @@ -838,6 +836,7 @@ export default class GMApi extends GM_Base { @GMContext.API({ depend: ["GM_closeNotification", "GM_updateNotification"], + alias: "GM.notification", }) public async GM_notification( detail: GMTypes.NotificationDetails | string, @@ -956,17 +955,17 @@ export default class GMApi extends GM_Base { }); } - @GMContext.API() + @GMContext.API({ alias: "GM.closeNotification" }) public GM_closeNotification(id: string): void { this.sendMessage("GM_closeNotification", [id]); } - @GMContext.API() + @GMContext.API({ alias: "GM.updateNotification" }) public GM_updateNotification(id: string, details: GMTypes.NotificationDetails): void { this.sendMessage("GM_updateNotification", [id, details]); } - @GMContext.API({ depend: ["GM_closeInTab"] }) + @GMContext.API({ depend: ["GM_closeInTab"], alias: "GM.openInTab" }) public GM_openInTab(url: string, options?: GMTypes.OpenTabOptions | boolean): GMTypes.Tab { let option: GMTypes.OpenTabOptions = {}; if (arguments.length === 1) { @@ -1019,19 +1018,19 @@ export default class GMApi extends GM_Base { return ret; } - @GMContext.API() + @GMContext.API({ alias: "GM.closeInTab" }) public GM_closeInTab(tabid: string) { return this.sendMessage("GM_closeInTab", [tabid]); } - @GMContext.API() + @GMContext.API({ alias: "GM.getTab" }) GM_getTab(callback: (data: any) => void) { this.sendMessage("GM_getTab", []).then((data) => { callback(data ?? {}); }); } - @GMContext.API() + @GMContext.API({ alias: "GM.saveTab" }) GM_saveTab(obj: object) { if (typeof obj === "object") { obj = JSON.parse(JSON.stringify(obj)); @@ -1039,14 +1038,14 @@ export default class GMApi extends GM_Base { this.sendMessage("GM_saveTab", [obj]); } - @GMContext.API() + @GMContext.API({ alias: "GM.getTabs" }) GM_getTabs(callback: (objs: { [key: string | number]: object }) => any) { this.sendMessage("GM_getTabs", []).then((resp) => { callback(resp); }); } - @GMContext.API() + @GMContext.API({ alias: "GM.setClipboard" }) GM_setClipboard(data: string, info?: string | { type?: string; minetype?: string }, cb?: () => void) { this.sendMessage("GM_setClipboard", [data, info]) .then(() => { diff --git a/src/app/service/content/gm_context.ts b/src/app/service/content/gm_context.ts index 0545b3945..b93728489 100644 --- a/src/app/service/content/gm_context.ts +++ b/src/app/service/content/gm_context.ts @@ -12,6 +12,13 @@ export function GMContextApiSet(grant: string, fnKey: string, api: any, param: A let m: ApiValue[] | undefined = apis.get(grant); if (!m) apis.set(grant, m = []); m.push({ fnKey, api, param }); + + // 如果有别名,也在别名下注册 API + if (param.alias) { + let aliasM: ApiValue[] | undefined = apis.get(param.alias); + if (!aliasM) apis.set(param.alias, aliasM = []); + aliasM.push({ fnKey, api, param }); + } } export const protect: { [key: string]: any } = {}; diff --git a/src/app/service/content/types.ts b/src/app/service/content/types.ts index e337af761..376bb6099 100644 --- a/src/app/service/content/types.ts +++ b/src/app/service/content/types.ts @@ -23,6 +23,7 @@ export type ValueUpdateData = { export interface ApiParam { follow?: string; depend?: string[]; + alias?: string; } export interface ApiValue { From b9a96215db0ccfc40e33f572c10ae70fa1d2215d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 10 Jul 2025 21:35:50 +0800 Subject: [PATCH 04/32] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=AB=E5=90=8D?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/exec_script.test.ts | 4 ++-- src/app/service/content/gm_context.ts | 21 ++++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/app/service/content/exec_script.test.ts b/src/app/service/content/exec_script.test.ts index b9fc92350..c041db9e7 100644 --- a/src/app/service/content/exec_script.test.ts +++ b/src/app/service/content/exec_script.test.ts @@ -228,10 +228,10 @@ describe("@grant GM", () => { expect(ret).toEqual({ "GM.getValue": expect.any(Function), "GM.getTab": expect.any(Function), - "GM.setTab": expect.any(Function), + "GM.saveTab": expect.any(Function), GM_getValue: undefined, GM_getTab: undefined, - GM_setTab: undefined, + GM_saveTab: undefined, }); }); }); diff --git a/src/app/service/content/gm_context.ts b/src/app/service/content/gm_context.ts index b93728489..78cd0e77a 100644 --- a/src/app/service/content/gm_context.ts +++ b/src/app/service/content/gm_context.ts @@ -10,21 +10,13 @@ export function GMContextApiGet(name: string): ApiValue[] | undefined { export function GMContextApiSet(grant: string, fnKey: string, api: any, param: ApiParam): void { // 一个 @grant 可以扩充多个 API 函数 let m: ApiValue[] | undefined = apis.get(grant); - if (!m) apis.set(grant, m = []); + if (!m) apis.set(grant, (m = [])); m.push({ fnKey, api, param }); - - // 如果有别名,也在别名下注册 API - if (param.alias) { - let aliasM: ApiValue[] | undefined = apis.get(param.alias); - if (!aliasM) apis.set(param.alias, aliasM = []); - aliasM.push({ fnKey, api, param }); - } } export const protect: { [key: string]: any } = {}; export default class GMContext { - public static protected(value: any = undefined) { return (target: any, propertyName: string) => { // keyword是与createContext时同步的,避免访问到context的内部变量 @@ -37,8 +29,15 @@ export default class GMContext { return (target: any, propertyName: string, descriptor: PropertyDescriptor) => { const key = propertyName; let { follow } = param; - if (!follow) follow = key; // follow 是实际 @grant 的权限 + const { alias } = param; + if (!follow) { + follow = key; // follow 是实际 @grant 的权限 + } GMContextApiSet(follow, key, descriptor.value, param); + if (alias) { + // 如果有别名,则使用别名 + GMContextApiSet(alias, alias, descriptor.value, param); + } }; } -} \ No newline at end of file +} From 13aee6702db664bcd810e640112fa989cfcbd6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 10 Jul 2025 22:25:31 +0800 Subject: [PATCH 05/32] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20GM=5Fcookie=20?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/gm_cookie.js | 5 +++ example/grant_none.js | 1 + src/app/service/content/create_context.ts | 17 ++++++++- src/app/service/content/exec_script.test.ts | 6 ++- src/app/service/content/gm_api.ts | 41 +++++++++++++++++---- 5 files changed, 61 insertions(+), 9 deletions(-) diff --git a/example/gm_cookie.js b/example/gm_cookie.js index 4fcb2e610..e9082b5af 100644 --- a/example/gm_cookie.js +++ b/example/gm_cookie.js @@ -6,6 +6,7 @@ // @author You // @match https://bbs.tampermonkey.net.cn/ // @grant GM_cookie +// @grant GM.cookie // @connect example.com // ==/UserScript== @@ -40,3 +41,7 @@ GM_cookie("set", { }) }); }); + +console.log("async GM.cookie.list", await GM.cookie.list({ + domain: "example.com" +})); diff --git a/example/grant_none.js b/example/grant_none.js index d23b3b565..6720b4a42 100644 --- a/example/grant_none.js +++ b/example/grant_none.js @@ -5,6 +5,7 @@ // @description try to take over the world! // @author You // @match https://bbs.tampermonkey.net.cn/ +// @grant none // ==/UserScript== console.log("Grant None", this, GM_info); diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 02a799758..5c8167bb3 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -46,7 +46,22 @@ export function createContext( g = g[part] || (g[part] = {}); } const finalPart = fnKeyArray[m]; - if (g[finalPart]) continue; + if (g[finalPart]) { + // 如果已存在且当前要设置的是一个函数,需要特殊处理 + // 保持现有的属性,同时让对象可调用 + if (typeof t.api === 'function' && typeof g[finalPart] === 'object') { + const existingObj = g[finalPart]; + const boundApi = t.api.bind(context); + // 创建一个可调用的对象,保留现有属性 + const callableObj = function(...args: any[]) { + return boundApi(...args); + }; + // 复制现有属性到新的可调用对象 + Object.assign(callableObj, existingObj); + g[finalPart] = callableObj; + } + continue; + } g[finalPart] = t.api.bind(context); const depend = t?.param?.depend; if (depend) { diff --git a/src/app/service/content/exec_script.test.ts b/src/app/service/content/exec_script.test.ts index c041db9e7..bb0c4b7b4 100644 --- a/src/app/service/content/exec_script.test.ts +++ b/src/app/service/content/exec_script.test.ts @@ -188,7 +188,7 @@ describe("none this", () => { describe("@grant GM", () => { it("GM_", async () => { const script = Object.assign({}, scriptRes2) as ScriptLoadInfo; - script.metadata.grant = ["GM_getValue", "GM_getTab", "GM_saveTab"]; + script.metadata.grant = ["GM_getValue", "GM_getTab", "GM_saveTab", "GM_cookie"]; // @ts-ignore const exec = new ExecScript(script, undefined, undefined, undefined, envInfo); script.code = `return { @@ -198,6 +198,8 @@ describe("@grant GM", () => { GM_getValue: this.GM_getValue, GM_getTab: this.GM_getTab, GM_saveTab: this.GM_saveTab, + GM_cookie: this.GM_cookie, + ["GM_cookie.list"]: this.GM_cookie.list, }`; exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -208,6 +210,8 @@ describe("@grant GM", () => { GM_getValue: expect.any(Function), GM_getTab: expect.any(Function), GM_saveTab: expect.any(Function), + GM_cookie: expect.any(Function), + ["GM_cookie.list"]: expect.any(Function), }); }); it("GM.*", async () => { diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index 425e3268e..59410568d 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -582,10 +582,32 @@ export default class GMApi extends GM_Base { // 用于脚本跨域请求,需要@connect domain指定允许的域名 @GMContext.API({ - alias: "GM.xmlHttpRequest", depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"], }) public GM_xmlhttpRequest(details: GMTypes.XHRDetails) { + return GMApi.GM_xmlhttpRequest(this, details); + } + + @GMContext.API({ depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"] }) + public ["GM.xmlHttpRequest"](details: GMTypes.XHRDetails): Promise { + let abort: { abort: () => void }; + const ret = new Promise((resolve, reject) => { + details.onloadend = (xhr: GMTypes.XHRResponse) => { + resolve(xhr); + }; + details.onerror = (error: any) => { + reject(error); + }; + abort = GMApi.GM_xmlhttpRequest(this, details); + }); + //@ts-ignore + ret.abort = () => { + abort && abort.abort && abort.abort(); + }; + return ret; + } + + static GM_xmlhttpRequest(a: GMApi, details: GMTypes.XHRDetails) { const u = new URL(details.url, window.location.href); if (details.headers) { Object.keys(details.headers).forEach((key) => { @@ -634,7 +656,7 @@ export default class GMApi extends GM_Base { return Promise.all( values.map(async (val) => { if (val instanceof File) { - const url = await this.CAT_createBlobUrl(val); + const url = await a.CAT_createBlobUrl(val); data.push({ key, type: "file", @@ -656,7 +678,7 @@ export default class GMApi extends GM_Base { } else if (details.data instanceof Blob) { // 处理blob param.dataType = "Blob"; - param.data = await this.CAT_createBlobUrl(details.data); + param.data = await a.CAT_createBlobUrl(details.data); } else { param.data = details.data; } @@ -678,11 +700,11 @@ export default class GMApi extends GM_Base { return async (xhr: GMTypes.XHRResponse) => { if (xhr.response) { if (responseType === "document") { - xhr.response = await this.CAT_fetchDocument(xhr.response); + xhr.response = await a.CAT_fetchDocument(xhr.response); xhr.responseXML = xhr.response; xhr.responseType = "document"; } else { - const resp = await this.CAT_fetchBlob(xhr.response); + const resp = await a.CAT_fetchBlob(xhr.response); if (responseType === "arraybuffer") { xhr.response = await resp.arrayBuffer(); } else { @@ -723,7 +745,7 @@ export default class GMApi extends GM_Base { } // 发送信息 - this.connect("GM_xmlhttpRequest", [param]).then((con) => { + a.connect("GM_xmlhttpRequest", [param]).then((con) => { connect = con; con.onMessage((data: { action: string; data: any }) => { // 处理返回 @@ -1045,7 +1067,7 @@ export default class GMApi extends GM_Base { }); } - @GMContext.API({ alias: "GM.setClipboard" }) + @GMContext.API({}) GM_setClipboard(data: string, info?: string | { type?: string; minetype?: string }, cb?: () => void) { this.sendMessage("GM_setClipboard", [data, info]) .then(() => { @@ -1060,6 +1082,11 @@ export default class GMApi extends GM_Base { }); } + @GMContext.API({ depend: ["GM_setClipboard"] }) + ["GM.setClipboard"](data: string, info?: string | { type?: string; minetype?: string }) { + return this.sendMessage("GM_setClipboard", [data, info]); + } + @GMContext.API() GM_getResourceText(name: string): string | undefined { if (!this.scriptRes.resource) { From 511555254cc14d00593259308992e758f94e40a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 10 Jul 2025 22:32:57 +0800 Subject: [PATCH 06/32] =?UTF-8?q?=F0=9F=A7=AA=20=E6=B7=BB=E5=8A=A0GM=20coo?= =?UTF-8?q?kie=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/exec_script.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/service/content/exec_script.test.ts b/src/app/service/content/exec_script.test.ts index bb0c4b7b4..7fab83ede 100644 --- a/src/app/service/content/exec_script.test.ts +++ b/src/app/service/content/exec_script.test.ts @@ -200,6 +200,7 @@ describe("@grant GM", () => { GM_saveTab: this.GM_saveTab, GM_cookie: this.GM_cookie, ["GM_cookie.list"]: this.GM_cookie.list, + ["GM.cookie"]: this.GM.cookie, }`; exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -212,11 +213,12 @@ describe("@grant GM", () => { GM_saveTab: expect.any(Function), GM_cookie: expect.any(Function), ["GM_cookie.list"]: expect.any(Function), + ["GM.cookie"]: undefined, }); }); it("GM.*", async () => { const script = Object.assign({}, scriptRes2) as ScriptLoadInfo; - script.metadata.grant = ["GM.getValue", "GM.getTab", "GM.saveTab"]; + script.metadata.grant = ["GM.getValue", "GM.getTab", "GM.saveTab", "GM.cookie"]; // @ts-ignore const exec = new ExecScript(script, undefined, undefined, undefined, envInfo); script.code = `return { @@ -226,6 +228,8 @@ describe("@grant GM", () => { GM_getValue: this.GM_getValue, GM_getTab: this.GM_getTab, GM_saveTab: this.GM_saveTab, + GM_cookie: this.GM_cookie, + ["GM.cookie"]: this.GM.cookie, }`; exec.scriptFunc = compileScript(compileScriptCode(script)); const ret = await exec.exec(); @@ -236,6 +240,8 @@ describe("@grant GM", () => { GM_getValue: undefined, GM_getTab: undefined, GM_saveTab: undefined, + GM_cookie: undefined, + ["GM.cookie"]: expect.any(Object), }); }); }); From 587557aaad1c31a66574c0366623508f9f7944af Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 11 Jul 2025 06:39:01 +0900 Subject: [PATCH 07/32] =?UTF-8?q?TreeShaking=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/create_context.ts | 4 +- src/app/service/content/gm_api.ts | 174 +++++++++++----------- 2 files changed, 91 insertions(+), 87 deletions(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 5c8167bb3..df5df382e 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from "uuid"; import type { Message } from "@Packages/message/types"; import EventEmitter from "eventemitter3"; import { GMContextApiGet } from "./gm_context"; -import { GM_Base } from "./gm_api"; +import { createGMBase } from "./gm_api"; // 构建沙盒上下文 export function createContext( @@ -16,7 +16,7 @@ export function createContext( // 按照GMApi构建 const valueChangeListener = new Map(); const EE: EventEmitter = new EventEmitter(); - const context = GM_Base.create({ + const context = createGMBase({ prefix: envPrefix, message, scriptRes, diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index 59410568d..5e2f04bd9 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -22,7 +22,7 @@ export interface IGM_Base { const integrity = {}; // 僅防止非法实例化 // GM_Base 定义内部用变量和函数。均使用@protected -export class GM_Base implements IGM_Base { +class GM_Base implements IGM_Base { @GMContext.protected() protected runFlag!: string; @@ -56,7 +56,7 @@ export class GM_Base implements IGM_Base { } @GMContext.protected() - static create(options: { [key: string]: any }) { + static createGMBase(options: { [key: string]: any }) { return new GM_Base(options, integrity) as GM_Base & { [key: string]: any }; } @@ -112,7 +112,7 @@ export default class GMApi extends GM_Base { /** * */ - notificationTagMap = new Map(); + notificationTagMap?: Map; constructor( public prefix: string, @@ -129,18 +129,14 @@ export default class GMApi extends GM_Base { scriptRes, valueChangeListener, EE, + notificationTagMap: new Map(), + eventId: 0, }, integrity ); } - // 获取脚本的值,可以通过@storageName让多个脚本共享一个储存空间 - @GMContext.API() - public GM_getValue(key: string, defaultValue?: any) { - return GMApi.GM_getValue(this, key, defaultValue); - } - - static GM_getValue(a: GMApi, key: string, defaultValue?: any) { + static _GM_getValue(a: GMApi, key: string, defaultValue?: any) { const ret = a.scriptRes.value[key]; if (ret !== undefined) { return ret; @@ -148,21 +144,22 @@ export default class GMApi extends GM_Base { return defaultValue; } + // 获取脚本的值,可以通过@storageName让多个脚本共享一个储存空间 + @GMContext.API() + public GM_getValue(key: string, defaultValue?: any) { + return _GM_getValue(this, key, defaultValue); + } + @GMContext.API() public ["GM.getValue"](key: string, defaultValue?: any): Promise { // 兼容GM.getValue return new Promise((resolve) => { - const ret = GMApi.GM_getValue(this, key, defaultValue); + const ret = _GM_getValue(this, key, defaultValue); resolve(ret); }); } - @GMContext.API() - public GM_setValue(key: string, value: any) { - GMApi.GM_setValue(this, key, value); - } - - static GM_setValue(a: GMApi, key: string, value: any) { + static _GM_setValue(a: GMApi, key: string, value: any) { // 对object的value进行一次转化 if (typeof value === "object") { value = JSON.parse(JSON.stringify(value)); @@ -176,25 +173,30 @@ export default class GMApi extends GM_Base { } } + @GMContext.API() + public GM_setValue(key: string, value: any) { + _GM_setValue(this, key, value); + } + @GMContext.API() public ["GM.setValue"](key: string, value: any): Promise { // Asynchronous wrapper for GM_setValue to support GM.setValue return new Promise((resolve) => { - GMApi.GM_setValue(this, key, value); + _GM_setValue(this, key, value); resolve(); }); } @GMContext.API() public GM_deleteValue(name: string): void { - GMApi.GM_setValue(this, name, undefined); + _GM_setValue(this, name, undefined); } @GMContext.API() public ["GM.deleteValue"](name: string): Promise { // Asynchronous wrapper for GM_deleteValue to support GM.deleteValue return new Promise((resolve) => { - GMApi.GM_setValue(this, name, undefined); + _GM_setValue(this, name, undefined); resolve(); }); } @@ -223,7 +225,7 @@ export default class GMApi extends GM_Base { } Object.keys(values).forEach((key) => { const value = values[key]; - GMApi.GM_setValue(this, key, value); + _GM_setValue(this, key, value); }); } @@ -248,7 +250,7 @@ export default class GMApi extends GM_Base { // Handle object with default values (e.g., { foo: 1, bar: 2, baz: 3 }) Object.keys(keysOrDefaults).forEach((key) => { const defaultValue = keysOrDefaults[key]; - result[key] = GMApi.GM_getValue(this, key, defaultValue); + result[key] = _GM_getValue(this, key, defaultValue); }); } return result; @@ -280,7 +282,7 @@ export default class GMApi extends GM_Base { return; } keys.forEach((key) => { - GMApi.GM_setValue(this, key, undefined); + _GM_setValue(this, key, undefined); }); } @@ -293,8 +295,6 @@ export default class GMApi extends GM_Base { }); } - eventId: number = 0; - @GMContext.API({ alias: "GM.addValueChangeListener" }) public GM_addValueChangeListener(name: string, listener: GMTypes.ValueChangeListener): number { this.eventId += 1; @@ -336,10 +336,29 @@ export default class GMApi extends GM_Base { return (this.message).getAndDelRelatedTarget(data.relatedTarget) as Document; } + static _GM_cookie( + a: IGM_Base, + action: string, + details: GMTypes.CookieDetails, + done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void + ) { + // 如果url和域名都没有,自动填充当前url + if (!details.url && !details.domain) { + details.url = window.location.href; + } + a.sendMessage("GM_cookie", [action, details]) + .then((resp: any) => { + done && done(resp, undefined); + }) + .catch((err) => { + done && done(undefined, err); + }); + } + @GMContext.API({ follow: "GM.cookie" }) ["GM.cookie.set"](details: GMTypes.CookieDetails) { return new Promise((resolve, reject) => { - GMApi.GM_cookie(this, "set", details, (cookie, error) => { + _GM_cookie(this, "set", details, (cookie, error) => { error ? reject(error) : resolve(cookie); }); }); @@ -348,7 +367,7 @@ export default class GMApi extends GM_Base { @GMContext.API({ follow: "GM.cookie" }) ["GM.cookie.list"](details: GMTypes.CookieDetails) { return new Promise((resolve, reject) => { - GMApi.GM_cookie(this, "list", details, (cookie, error) => { + _GM_cookie(this, "list", details, (cookie, error) => { error ? reject(error) : resolve(cookie); }); }); @@ -357,7 +376,7 @@ export default class GMApi extends GM_Base { @GMContext.API({ follow: "GM.cookie" }) ["GM.cookie.delete"](details: GMTypes.CookieDetails) { return new Promise((resolve, reject) => { - GMApi.GM_cookie(this, "delete", details, (cookie, error) => { + _GM_cookie(this, "delete", details, (cookie, error) => { error ? reject(error) : resolve(cookie); }); }); @@ -368,7 +387,7 @@ export default class GMApi extends GM_Base { details: GMTypes.CookieDetails, done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void ) { - GMApi.GM_cookie(this, "set", details, done); + _GM_cookie(this, "set", details, done); } @GMContext.API({ follow: "GM_cookie" }) @@ -376,7 +395,7 @@ export default class GMApi extends GM_Base { details: GMTypes.CookieDetails, done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void ) { - GMApi.GM_cookie(this, "list", details, done); + _GM_cookie(this, "list", details, done); } @GMContext.API({ follow: "GM_cookie" }) @@ -384,7 +403,7 @@ export default class GMApi extends GM_Base { details: GMTypes.CookieDetails, done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void ) { - GMApi.GM_cookie(this, "delete", details, done); + _GM_cookie(this, "delete", details, done); } @GMContext.API() @@ -393,26 +412,7 @@ export default class GMApi extends GM_Base { details: GMTypes.CookieDetails, done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void ) { - GMApi.GM_cookie(this, action, details, done); - } - - static GM_cookie( - a: IGM_Base, - action: string, - details: GMTypes.CookieDetails, - done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void - ) { - // 如果url和域名都没有,自动填充当前url - if (!details.url && !details.domain) { - details.url = window.location.href; - } - a.sendMessage("GM_cookie", [action, details]) - .then((resp: any) => { - done && done(resp, undefined); - }) - .catch((err) => { - done && done(undefined, err); - }); + _GM_cookie(this, action, details, done); } menuMap: Map | undefined; @@ -580,34 +580,7 @@ export default class GMApi extends GM_Base { }); } - // 用于脚本跨域请求,需要@connect domain指定允许的域名 - @GMContext.API({ - depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"], - }) - public GM_xmlhttpRequest(details: GMTypes.XHRDetails) { - return GMApi.GM_xmlhttpRequest(this, details); - } - - @GMContext.API({ depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"] }) - public ["GM.xmlHttpRequest"](details: GMTypes.XHRDetails): Promise { - let abort: { abort: () => void }; - const ret = new Promise((resolve, reject) => { - details.onloadend = (xhr: GMTypes.XHRResponse) => { - resolve(xhr); - }; - details.onerror = (error: any) => { - reject(error); - }; - abort = GMApi.GM_xmlhttpRequest(this, details); - }); - //@ts-ignore - ret.abort = () => { - abort && abort.abort && abort.abort(); - }; - return ret; - } - - static GM_xmlhttpRequest(a: GMApi, details: GMTypes.XHRDetails) { + static _GM_xmlhttpRequest(a: GMApi, details: GMTypes.XHRDetails) { const u = new URL(details.url, window.location.href); if (details.headers) { Object.keys(details.headers).forEach((key) => { @@ -797,6 +770,33 @@ export default class GMApi extends GM_Base { }; } + // 用于脚本跨域请求,需要@connect domain指定允许的域名 + @GMContext.API({ + depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"], + }) + public GM_xmlhttpRequest(details: GMTypes.XHRDetails) { + return _GM_xmlhttpRequest(this, details); + } + + @GMContext.API({ depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"] }) + public ["GM.xmlHttpRequest"](details: GMTypes.XHRDetails): Promise { + let abort: { abort: () => void }; + const ret = new Promise((resolve, reject) => { + details.onloadend = (xhr: GMTypes.XHRResponse) => { + resolve(xhr); + }; + details.onerror = (error: any) => { + reject(error); + }; + abort = _GM_xmlhttpRequest(this, details); + }); + //@ts-ignore + ret.abort = () => { + abort && abort.abort && abort.abort(); + }; + return ret; + } + @GMContext.API({ alias: "GM.download" }) GM_download(url: GMTypes.DownloadDetails | string, filename?: string): GMTypes.AbortHandle { let details: GMTypes.DownloadDetails; @@ -866,9 +866,7 @@ export default class GMApi extends GM_Base { image?: string, onclick?: GMTypes.NotificationOnClick ) { - if (this.notificationTagMap == null) { - this.notificationTagMap = new Map(); - } + const notificationTagMap: Map = this.notificationTagMap || (this.notificationTagMap = new Map()); this.eventId += 1; let data: GMTypes.NotificationDetails; if (typeof detail === "string") { @@ -908,14 +906,14 @@ export default class GMApi extends GM_Base { } let notificationId: string | undefined = undefined; if (typeof data.tag === "string") { - notificationId = this.notificationTagMap.get(data.tag); + notificationId = notificationTagMap.get(data.tag); } this.sendMessage("GM_notification", [data, notificationId]).then((id) => { if (create) { create.apply({ id }, [id]); } if (typeof data.tag === "string") { - this.notificationTagMap.set(data.tag, id); + notificationTagMap.set(data.tag, id); } let isPreventDefault = false; this.EE.addListener("GM_notification:" + id, (resp: NotificationMessageOption) => { @@ -924,7 +922,7 @@ export default class GMApi extends GM_Base { */ const clearNotificationIdMap = () => { if (typeof data.tag === "string") { - this.notificationTagMap.delete(data.tag); + notificationTagMap.delete(data.tag); } }; switch (resp.event) { @@ -1143,3 +1141,9 @@ export default class GMApi extends GM_Base { return this.sendMessage("windowDotFocus", []); } } + +// 從 GM_Base 對象中解構出 createGMBase 函数並導出(可供其他模塊使用) +export const { createGMBase } = GM_Base; + +// 從 GMApi 對象中解構出內部函數,用於後續本地使用,不導出 +const { _GM_getValue, _GM_cookie, _GM_setValue, _GM_xmlhttpRequest } = GMApi; From 2f05d2bf5c330dba332bc72fe0b65596a7560fd1 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 11 Jul 2025 07:09:11 +0900 Subject: [PATCH 08/32] createStubCallable & defaultFn --- src/app/service/content/create_context.ts | 28 +++++++---------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index df5df382e..bb29f71e6 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -31,6 +31,11 @@ export function createContext( }, grantSet: new Set(), }); + const createStubCallable = () => function (this: { [key: string]: any }) { + const f = this.defaultFn; + if (!f) throw new Error("this stub is not callable."); + return f(context); + } const __methodInject__ = (grant: string): boolean => { const grantSet: Set = context.grantSet; const s = GMContextApiGet(grant); @@ -39,30 +44,13 @@ export function createContext( grantSet.add(grant); for (const t of s) { const fnKeyArray = t.fnKey.split("."); - const m = fnKeyArray.length - 1; + const m = fnKeyArray.length; let g = context; for (let i = 0; i < m; i++) { const part = fnKeyArray[i]; - g = g[part] || (g[part] = {}); - } - const finalPart = fnKeyArray[m]; - if (g[finalPart]) { - // 如果已存在且当前要设置的是一个函数,需要特殊处理 - // 保持现有的属性,同时让对象可调用 - if (typeof t.api === 'function' && typeof g[finalPart] === 'object') { - const existingObj = g[finalPart]; - const boundApi = t.api.bind(context); - // 创建一个可调用的对象,保留现有属性 - const callableObj = function(...args: any[]) { - return boundApi(...args); - }; - // 复制现有属性到新的可调用对象 - Object.assign(callableObj, existingObj); - g[finalPart] = callableObj; - } - continue; + g = g[part] || (g[part] = createStubCallable()); // 建立占位函数物件 } - g[finalPart] = t.api.bind(context); + g.defaultFn = t.api; // 定义占位函数物件的实作行为 const depend = t?.param?.depend; if (depend) { for (const grant of depend) { From a139307e1fb3d79575f2fa63f8d567f65711dcc8 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 11 Jul 2025 07:19:34 +0900 Subject: [PATCH 09/32] =?UTF-8?q?=E6=B3=A8=E9=87=8A=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/create_context.ts | 2 ++ src/app/service/content/gm_api.ts | 3 ++- src/app/service/content/gm_context.ts | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index bb29f71e6..21cb1b30f 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -31,6 +31,8 @@ export function createContext( }, grantSet: new Set(), }); + // 若考虑完全禁止外部查阅 API 的实作,应考虑 defaultFnMap.get(this) + // 现在没有这个理由,则使用性能较高的 .defaultFn const createStubCallable = () => function (this: { [key: string]: any }) { const f = this.defaultFn; if (!f) throw new Error("this stub is not callable."); diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index 5e2f04bd9..d686be2aa 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -22,6 +22,7 @@ export interface IGM_Base { const integrity = {}; // 僅防止非法实例化 // GM_Base 定义内部用变量和函数。均使用@protected +// 暂不考虑 Object.getOwnPropertyNames(GM_Base.prototype) 和 ts-morph 脚本生成 class GM_Base implements IGM_Base { @GMContext.protected() protected runFlag!: string; @@ -119,7 +120,7 @@ export default class GMApi extends GM_Base { public message: Message, public scriptRes: ScriptRunResource ) { - // testing only + // testing only 仅供测试用 const valueChangeListener = new Map(); const EE: EventEmitter = new EventEmitter(); super( diff --git a/src/app/service/content/gm_context.ts b/src/app/service/content/gm_context.ts index 78cd0e77a..29b62eeaf 100644 --- a/src/app/service/content/gm_context.ts +++ b/src/app/service/content/gm_context.ts @@ -3,7 +3,7 @@ import type { ApiParam, ApiValue } from "./types"; const apis: Map = new Map(); export function GMContextApiGet(name: string): ApiValue[] | undefined { - // 回傳 Api 列表 + // 回传 Api 列表 return apis.get(name); } @@ -31,11 +31,11 @@ export default class GMContext { let { follow } = param; const { alias } = param; if (!follow) { - follow = key; // follow 是实际 @grant 的权限 + follow = key; // follow 是实际 @grant 的权限;使用follow时,不要使用alias以避免混乱 } GMContextApiSet(follow, key, descriptor.value, param); if (alias) { - // 如果有别名,则使用别名 + // 追加别名呼叫(参数和回传完全一致,为 GM_xxx 与 GM.xxx 等问题设计) GMContextApiSet(alias, alias, descriptor.value, param); } }; From 703cf02d071efebdbb171c1582f0aea4c8c5d599 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 11 Jul 2025 08:06:11 +0900 Subject: [PATCH 10/32] =?UTF-8?q?=E7=AE=80=E5=8D=95=E4=BF=AE=E4=B8=80?= =?UTF-8?q?=E4=B8=8B=EF=BC=8C=E6=97=A0=E8=A1=8C=E4=B8=BA=E6=94=B9=E5=8F=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/gm_api.ts | 4 +- src/app/service/content/gm_context.ts | 2 +- src/app/service/service_worker/gm_api.ts | 20 +++--- .../service_worker/permission_verify.ts | 71 ++++++++++--------- 4 files changed, 52 insertions(+), 45 deletions(-) diff --git a/src/app/service/content/gm_api.ts b/src/app/service/content/gm_api.ts index d686be2aa..86b73cb62 100644 --- a/src/app/service/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -1134,12 +1134,12 @@ export default class GMApi extends GM_Base { @GMContext.API() ["window.close"]() { - return this.sendMessage("windowDotClose", []); + return this.sendMessage("window.close", []); } @GMContext.API() ["window.focus"]() { - return this.sendMessage("windowDotFocus", []); + return this.sendMessage("window.focus", []); } } diff --git a/src/app/service/content/gm_context.ts b/src/app/service/content/gm_context.ts index 29b62eeaf..13564a578 100644 --- a/src/app/service/content/gm_context.ts +++ b/src/app/service/content/gm_context.ts @@ -7,7 +7,7 @@ export function GMContextApiGet(name: string): ApiValue[] | undefined { return apis.get(name); } -export function GMContextApiSet(grant: string, fnKey: string, api: any, param: ApiParam): void { +function GMContextApiSet(grant: string, fnKey: string, api: any, param: ApiParam): void { // 一个 @grant 可以扩充多个 API 函数 let m: ApiValue[] | undefined = apis.get(grant); if (!m) apis.set(grant, (m = [])); diff --git a/src/app/service/service_worker/gm_api.ts b/src/app/service/service_worker/gm_api.ts index 0f0891108..e3d44810a 100644 --- a/src/app/service/service_worker/gm_api.ts +++ b/src/app/service/service_worker/gm_api.ts @@ -8,7 +8,7 @@ import { type MessageQueue } from "@Packages/message/message_queue"; import { MockMessageConnect } from "@Packages/message/mock_message"; import { type ValueService } from "@App/app/service/service_worker/value"; import type { ConfirmParam } from "./permission_verify"; -import PermissionVerify from "./permission_verify"; +import PermissionVerify, { PermissionVerifyApiGet } from "./permission_verify"; import Cache, { incr } from "@App/app/cache"; import EventEmitter from "eventemitter3"; import { type RuntimeService } from "./runtime"; @@ -133,7 +133,7 @@ export default class GMApi { async handlerRequest(data: MessageRequest, sender: GetSender) { this.logger.trace("GM API request", { api: data.api, uuid: data.uuid, param: data.params }); - const api = PermissionVerify.apis.get(data.api); + const api = PermissionVerifyApiGet(data.api); if (!api) { throw new Error("gm api is not found"); } @@ -159,7 +159,7 @@ export default class GMApi { } @PermissionVerify.API({ - async confirm(request: Request) { + confirm: async (request: Request) => { if (request.params[0] === "store") { return true; } @@ -812,7 +812,7 @@ export default class GMApi { } @PermissionVerify.API({ - link: "GM_openInTab", + link: ["GM_openInTab"], }) async GM_closeInTab(request: Request): Promise { try { @@ -939,7 +939,7 @@ export default class GMApi { } @PermissionVerify.API({ - link: "GM_notification", + link: ["GM_notification"], }) GM_closeNotification(request: Request) { if (request.params.length === 0) { @@ -951,7 +951,7 @@ export default class GMApi { } @PermissionVerify.API({ - link: "GM_notification", + link: ["GM_notification"], }) GM_updateNotification(request: Request) { if (isFirefox()) { @@ -1055,8 +1055,8 @@ export default class GMApi { await sendMessage(this.send, "offscreen/gmApi/setClipboard", { data, type: clipboardType }); } - @PermissionVerify.API({ alias: ["window.close"] }) - async windowDotClose(request: Request, sender: GetSender) { + @PermissionVerify.API() + async ["window.close"](request: Request, sender: GetSender) { /* * Note: for security reasons it is not allowed to close the last tab of a window. * https://www.tampermonkey.net/documentation.php#api:window.close @@ -1066,8 +1066,8 @@ export default class GMApi { await chrome.tabs.remove(sender.getSender().tab?.id as number); } - @PermissionVerify.API({ alias: ["window.focus"] }) - async windowDotFocus(request: Request, sender: GetSender) { + @PermissionVerify.API() + async ["window.focus"](request: Request, sender: GetSender) { await chrome.tabs.update(sender.getSender().tab?.id as number, { active: true, }); diff --git a/src/app/service/service_worker/permission_verify.ts b/src/app/service/service_worker/permission_verify.ts index 22671bae4..6b9d0cf11 100644 --- a/src/app/service/service_worker/permission_verify.ts +++ b/src/app/service/service_worker/permission_verify.ts @@ -32,17 +32,21 @@ export interface UserConfirm { type: number; // 1: 允许一次 2: 临时允许全部 3: 临时允许此 4: 永久允许全部 5: 永久允许此 } +export type ApiParamConfirmFn = (request: Request) => Promise; + export interface ApiParam { - // 默认提供的函数 + // 默认提供的函数(未使用?) default?: boolean; // 是否只有后台环境中才能执行 background?: boolean; // 是否需要弹出页面让用户进行确认 - confirm?: (request: Request) => Promise; + confirm?: ApiParamConfirmFn; // 别名 alias?: string[]; // 关联 - link?: string | string[]; + link?: string[]; + // 兼容GM.* 及 CAT.* + dotAlias?: boolean } export interface ApiValue { @@ -50,27 +54,31 @@ export interface ApiValue { param: ApiParam; } -export interface IPermissionVerify { - verify(request: Request, api: ApiValue): Promise; +const apis: Map = new Map(); + +export function PermissionVerifyApiGet(name: string): ApiValue | undefined { + return apis.get(name); +} + +function PermissionVerifyApiSet(key: string, api: any, param: ApiParam): void { + apis.set(key, { api, param }); } export default class PermissionVerify { - static apis: Map = new Map(); public static API(param: ApiParam = {}) { + if (param.dotAlias === undefined) { + param.dotAlias = true; // 预设兼容GM.* 及 CAT.* + } return (target: any, propertyName: string, descriptor: PropertyDescriptor) => { const key = propertyName; - PermissionVerify.apis.set(key, { - api: descriptor.value, - param, - }); - // 兼容GM.* - const dot = key.replace("_", "."); - if (dot !== key) { - PermissionVerify.apis.set(dot, { - api: descriptor.value, - param, - }); + PermissionVerifyApiSet(key, + descriptor.value, + param + ); + // 兼容GM.* 及 CAT.* + if (param.dotAlias && key.includes('_')) { + const dot = key.replace("_", "."); if (param.alias) { param.alias.push(dot); } else { @@ -80,12 +88,12 @@ export default class PermissionVerify { // 处理别名 if (param.alias) { - param.alias.forEach((alias) => { - PermissionVerify.apis.set(alias, { - api: descriptor.value, - param, - }); - }); + for (const alias of param.alias) { + PermissionVerifyApiSet(alias, + descriptor.value, + param + ); + } } }; } @@ -109,7 +117,8 @@ export default class PermissionVerify { // 验证是否有权限 async verify(request: Request, api: ApiValue): Promise { - if (api.param.default) { + const { alias, link, confirm } = api.param; + if (api.param.default) { // (未使用?) return true; } // 没有其它条件,从metadata.grant中判断 @@ -123,16 +132,14 @@ export default class PermissionVerify { // 名称相等 grantName === request.api || // 别名相等 - (api.param.alias && api.param.alias.includes(grantName)) || - // 有关联的 - (typeof api.param.link === "string" && grantName === api.param.link) || + (alias && alias.includes(grantName)) || // 关联包含 - (Array.isArray(api.param.link) && api.param.link.includes(grantName)) + (link && link.includes(grantName)) ) { // 需要用户确认 let result = true; - if (api.param.confirm) { - result = await this.pushConfirmQueue(request, api); + if (confirm) { + result = await this.pushConfirmQueue(request, confirm); } return result; } @@ -157,8 +164,8 @@ export default class PermissionVerify { } // 确认队列,为了防止一次性打开过多的窗口 - async pushConfirmQueue(request: Request, api: ApiValue): Promise { - const confirm = await api.param.confirm!(request); + async pushConfirmQueue(request: Request, confirmFn: ApiParamConfirmFn): Promise { + const confirm = await confirmFn(request); if (confirm === true) { return true; } From 6c07e1e07cc06d7de1b8b9a853420e1d65ad2616 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 11 Jul 2025 08:07:17 +0900 Subject: [PATCH 11/32] =?UTF-8?q?=E6=B2=92=E6=9C=89CAT.*?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/permission_verify.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/service/service_worker/permission_verify.ts b/src/app/service/service_worker/permission_verify.ts index 6b9d0cf11..09ec84c35 100644 --- a/src/app/service/service_worker/permission_verify.ts +++ b/src/app/service/service_worker/permission_verify.ts @@ -45,7 +45,7 @@ export interface ApiParam { alias?: string[]; // 关联 link?: string[]; - // 兼容GM.* 及 CAT.* + // 兼容GM.* dotAlias?: boolean } @@ -68,7 +68,7 @@ export default class PermissionVerify { public static API(param: ApiParam = {}) { if (param.dotAlias === undefined) { - param.dotAlias = true; // 预设兼容GM.* 及 CAT.* + param.dotAlias = true; // 预设兼容GM.* } return (target: any, propertyName: string, descriptor: PropertyDescriptor) => { const key = propertyName; @@ -76,9 +76,9 @@ export default class PermissionVerify { descriptor.value, param ); - // 兼容GM.* 及 CAT.* - if (param.dotAlias && key.includes('_')) { - const dot = key.replace("_", "."); + // 兼容GM.* + if (param.dotAlias && key.includes('GM_')) { + const dot = key.replace("GM_", "GM."); if (param.alias) { param.alias.push(dot); } else { From ae68ef48b4e26e49155527b796bc753edffa5219 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 11 Jul 2025 08:21:32 +0900 Subject: [PATCH 12/32] indexOf -> includes --- packages/filesystem/onedrive/onedrive.ts | 2 +- src/app/service/offscreen/gm_api.ts | 2 +- src/app/service/sandbox/runtime.ts | 2 +- src/app/service/service_worker/gm_api.ts | 2 +- src/app/service/service_worker/runtime.ts | 2 +- src/pages/components/ScriptResource/index.tsx | 2 +- src/pages/components/ScriptStorage/index.tsx | 2 +- src/pages/options/routes/Logger.tsx | 4 ++-- src/pages/options/routes/SubscribeList.tsx | 2 +- src/pages/options/routes/utils.tsx | 8 ++++---- src/pkg/utils/cron.ts | 2 +- src/pkg/utils/match.ts | 2 +- src/pkg/utils/script.ts | 4 ++-- src/pkg/utils/utils.ts | 2 +- 14 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/filesystem/onedrive/onedrive.ts b/packages/filesystem/onedrive/onedrive.ts index d6c9cb92c..3a92ca343 100644 --- a/packages/filesystem/onedrive/onedrive.ts +++ b/packages/filesystem/onedrive/onedrive.ts @@ -73,7 +73,7 @@ export default class OneDriveFileSystem implements FileSystem { request(url: string, config?: RequestInit, nothen?: boolean): Promise { config = config || {}; const headers = config.headers || new Headers(); - if (url.indexOf("uploadSession") === -1) { + if (!url.includes("uploadSession")) { headers.append(`Authorization`, `Bearer ${this.accessToken}`); } config.headers = headers; diff --git a/src/app/service/offscreen/gm_api.ts b/src/app/service/offscreen/gm_api.ts index 53025e796..d66788d7f 100644 --- a/src/app/service/offscreen/gm_api.ts +++ b/src/app/service/offscreen/gm_api.ts @@ -37,7 +37,7 @@ export default class GMApi { response.response = URL.createObjectURL(blob); } try { - if (xhr.getResponseHeader("Content-Type")?.indexOf("text") !== -1) { + if (xhr.getResponseHeader("Content-Type")?.includes("text")) { // 如果是文本类型,则尝试转换为文本 response.responseText = await blob.text(); } diff --git a/src/app/service/sandbox/runtime.ts b/src/app/service/sandbox/runtime.ts index 8b1dc5c7b..b52dd759a 100644 --- a/src/app/service/sandbox/runtime.ts +++ b/src/app/service/sandbox/runtime.ts @@ -180,7 +180,7 @@ export class Runtime { script.metadata.crontab.forEach((val) => { let oncePos = 0; let crontab = val; - if (crontab.indexOf("once") !== -1) { + if (crontab.includes("once")) { const vals = crontab.split(" "); vals.forEach((item, index) => { if (item === "once") { diff --git a/src/app/service/service_worker/gm_api.ts b/src/app/service/service_worker/gm_api.ts index e3d44810a..b944a280f 100644 --- a/src/app/service/service_worker/gm_api.ts +++ b/src/app/service/service_worker/gm_api.ts @@ -178,7 +178,7 @@ export default class GMApi { if (request.script.metadata.connect) { const { connect } = request.script.metadata; flag = - connect.indexOf("*") !== -1 || + connect.includes("*") || connect.findIndex((connectHostName) => url.hostname.endsWith(connectHostName)) !== -1; } if (!flag) { diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 7abd30359..ba98a04c2 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -593,7 +593,7 @@ export class RuntimeService { await chrome.userScripts.register(scripts); } catch (e: any) { this.logger.error("register inject.js error", Logger.E(e)); - if (e.message?.indexOf("Duplicate script ID") !== -1) { + if (e.message?.includes("Duplicate script ID")) { // 如果是重复注册, 则更新 try { await chrome.userScripts.update(scripts); diff --git a/src/pages/components/ScriptResource/index.tsx b/src/pages/components/ScriptResource/index.tsx index 13645d059..6aa18e83e 100644 --- a/src/pages/components/ScriptResource/index.tsx +++ b/src/pages/components/ScriptResource/index.tsx @@ -68,7 +68,7 @@ const ScriptResource: React.FC<{ ); }, - onFilter: (value, row) => (value ? row.key.indexOf(value) !== -1 : true), + onFilter: (value, row) => (!value || row.key.includes(value)), onFilterDropdownVisibleChange: (v) => { if (v) { setTimeout(() => inputRef.current!.focus(), 150); diff --git a/src/pages/components/ScriptStorage/index.tsx b/src/pages/components/ScriptStorage/index.tsx index b53cdf663..f6eec8eb6 100644 --- a/src/pages/components/ScriptStorage/index.tsx +++ b/src/pages/components/ScriptStorage/index.tsx @@ -107,7 +107,7 @@ const ScriptStorage: React.FC<{ ); }, - onFilter: (value, row) => (value ? row.key.indexOf(value) !== -1 : true), + onFilter: (value, row) => (!value || row.key.includes(value)), onFilterDropdownVisibleChange: (v) => { if (v) { setTimeout(() => inputRef.current!.focus(), 150); diff --git a/src/pages/options/routes/Logger.tsx b/src/pages/options/routes/Logger.tsx index 2b28ead88..9038ad974 100644 --- a/src/pages/options/routes/Logger.tsx +++ b/src/pages/options/routes/Logger.tsx @@ -40,7 +40,7 @@ function LoggerPage() { } break; case "=~": - if (typeof value === "string" && value.indexOf(query.value) === -1) { + if (typeof value === "string" && !value.includes(query.value)) { return; } break; @@ -50,7 +50,7 @@ function LoggerPage() { } break; case "!~": - if (typeof value === "string" && value.indexOf(query.value) === -1) { + if (typeof value === "string" && !value.includes(query.value)) { return; } break; diff --git a/src/pages/options/routes/SubscribeList.tsx b/src/pages/options/routes/SubscribeList.tsx index a187bb5af..fe7dea84e 100644 --- a/src/pages/options/routes/SubscribeList.tsx +++ b/src/pages/options/routes/SubscribeList.tsx @@ -108,7 +108,7 @@ function SubscribeList() { ); }, - onFilter: (value, row) => (value ? row.name.indexOf(value) !== -1 : true), + onFilter: (value, row) => (!value || row.name.includes(value)), onFilterDropdownVisibleChange: (visible) => { if (visible) { setTimeout(() => inputRef.current!.focus(), 150); diff --git a/src/pages/options/routes/utils.tsx b/src/pages/options/routes/utils.tsx index 962755bb8..06ff3ed59 100644 --- a/src/pages/options/routes/utils.tsx +++ b/src/pages/options/routes/utils.tsx @@ -20,7 +20,7 @@ export function scriptListSort(result: Script[]) { export function installUrlToHome(installUrl: string) { try { // 解析scriptcat - if (installUrl.indexOf("scriptcat.org") !== -1) { + if (installUrl.includes("scriptcat.org")) { const id = installUrl.split("/")[5]; return ( ); } - if (installUrl.indexOf("greasyfork.org") !== -1) { + if (installUrl.includes("greasyfork.org")) { const id = installUrl.split("/")[4]; return ( ); } - if (installUrl.indexOf("raw.githubusercontent.com") !== -1) { + if (installUrl.includes("raw.githubusercontent.com")) { const repo = `${installUrl.split("/")[3]}/${installUrl.split("/")[4]}`; return (