处理后台脚本API

This commit is contained in:
2025-04-10 18:07:35 +08:00
parent 239f961485
commit a2870eb18e
31 changed files with 767 additions and 1436 deletions

View File

@ -0,0 +1,62 @@
import { ScriptRunResouce } from "@App/app/repo/scripts";
import { Client, sendMessage } from "@Packages/message/client";
import { forwardMessage, Message, MessageSend, Server } from "@Packages/message/server";
// content页的处理
export default class ContentRuntime {
constructor(
private extServer: Server,
private server: Server,
private extSend: MessageSend,
private msg: Message
) {}
start(scripts: ScriptRunResouce[]) {
this.extServer.on("runtime/emitEvent", (data) => {
// 转发给inject
return sendMessage(this.msg, "inject/runtime/emitEvent", data);
});
this.extServer.on("runtime/valueUpdate", (data) => {
// 转发给inject
return sendMessage(this.msg, "inject/runtime/valueUpdate", data);
});
forwardMessage(
"serviceWorker",
"runtime/gmApi",
this.server,
this.extSend,
(data: { api: string; params: any }) => {
// 拦截关注的api
switch (data.api) {
case "CAT_createBlobUrl": {
const file = data.params[0] as File;
const url = URL.createObjectURL(file);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 60 * 1000);
return Promise.resolve(url);
}
case "CAT_fetchBlob": {
return fetch(data.params[0]).then((res) => res.blob());
}
case "CAT_fetchDocument": {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.responseType = "document";
xhr.open("GET", data.params[0]);
xhr.onload = () => {
resolve({
relatedTarget: xhr.response,
});
};
xhr.send();
});
}
}
return Promise.resolve(false);
}
);
const client = new Client(this.msg, "inject");
client.do("pageLoad", { scripts });
}
}

View File

@ -0,0 +1,121 @@
import { ScriptRunResouce } from "@App/app/repo/scripts";
import ExecScript from "./exec_script";
import { compileScript, compileScriptCode } from "./utils";
import { ExtVersion } from "@App/app/const";
import { initTestEnv } from "@Tests/utils";
import { describe, expect, it } from "vitest";
initTestEnv();
const scriptRes = {
id: 0,
name: "test",
metadata: {
version: ["1.0.0"],
},
code: "console.log('test')",
sourceCode: "sourceCode",
value: {},
grantMap: {
none: true,
},
} as unknown as ScriptRunResouce;
// @ts-ignore
const noneExec = new ExecScript(scriptRes);
const scriptRes2 = {
id: 0,
name: "test",
metadata: {
version: ["1.0.0"],
},
code: "console.log('test')",
sourceCode: "sourceCode",
value: {},
grantMap: {},
} as unknown as ScriptRunResouce;
// @ts-ignore
const sandboxExec = new ExecScript(scriptRes2);
describe("GM_info", () => {
it("none", async () => {
scriptRes.code = "return GM_info";
noneExec.scriptFunc = compileScript(compileScriptCode(scriptRes));
const ret = await noneExec.exec();
expect(ret.version).toEqual(ExtVersion);
expect(ret.script.version).toEqual("1.0.0");
});
it("sandbox", async () => {
scriptRes2.code = "return GM_info";
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
const ret = await sandboxExec.exec();
expect(ret.version).toEqual(ExtVersion);
expect(ret.script.version).toEqual("1.0.0");
});
});
describe("unsafeWindow", () => {
it("sandbox", async () => {
// @ts-ignore
global.testUnsafeWindow = "ok";
scriptRes2.code = "return unsafeWindow.testUnsafeWindow";
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
const ret = await sandboxExec.exec();
expect(ret).toEqual("ok");
scriptRes2.code = "return window.testUnsafeWindow";
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
const ret2 = await sandboxExec.exec();
expect(ret2).toEqual(undefined);
});
});
describe("sandbox", () => {
it("global", async () => {
scriptRes2.code = "window.testObj = 'ok';return window.testObj";
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
let ret = await sandboxExec.exec();
expect(ret).toEqual("ok");
scriptRes2.code = "window.testObj = 'ok2';return testObj";
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
ret = await sandboxExec.exec();
expect(ret).toEqual("ok2");
});
it("this", async () => {
scriptRes2.code = "this.testObj='ok2';return testObj;";
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
const ret = await sandboxExec.exec();
expect(ret).toEqual("ok2");
});
it("this2", async () => {
scriptRes2.code = `
!function(t, e) {
"object" == typeof exports ? module.exports = exports = e() : "function" == typeof define && define.amd ? define([], e) : t.CryptoJS = e()
console.log("object" == typeof exports,"function" == typeof define)
} (this, function () {
return { test: "ok3" }
});
console.log(CryptoJS)
return CryptoJS.test;`;
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
const ret = await sandboxExec.exec();
expect(ret).toEqual("ok3");
});
// 沉浸式翻译, 常量值被改变
it("NodeFilter #214", async () => {
scriptRes2.code = `return NodeFilter.FILTER_REJECT;`;
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
const ret = await sandboxExec.exec();
expect(ret).toEqual(2);
});
// RegExp.$x 内容被覆盖 https://github.com/scriptscat/scriptcat/issues/293
it("RegExp", async () => {
scriptRes2.code = `let ok = /12(3)/.test('123');return RegExp.$1;`;
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
const ret = await sandboxExec.exec();
expect(ret).toEqual("3");
});
});

View File

@ -0,0 +1,93 @@
import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { ScriptRunResouce } from "@App/app/repo/scripts";
import GMApi from "./gm_api";
import { compileScript, createContext, proxyContext, ScriptFunc } from "./utils";
import { Message } from "@Packages/message/server";
export type ValueUpdateSender = {
runFlag: string;
tabId?: number;
};
export type ValueUpdateData = {
oldValue: any;
value: any;
key: string; // 值key
uuid: string;
storageName: string; // 储存name
sender: ValueUpdateSender;
};
export class RuntimeMessage {}
// 执行脚本,控制脚本执行与停止
export default class ExecScript {
scriptRes: ScriptRunResouce;
scriptFunc: ScriptFunc;
logger: Logger;
proxyContent: any;
sandboxContent?: GMApi;
GM_info: any;
constructor(
scriptRes: ScriptRunResouce,
envPrefix: "content" | "offscreen",
message: Message,
code: string | ScriptFunc,
thisContext?: { [key: string]: any }
) {
this.scriptRes = scriptRes;
this.logger = LoggerCore.getInstance().logger({
component: "exec",
script: this.scriptRes.uuid,
name: this.scriptRes.name,
});
this.GM_info = GMApi.GM_info(this.scriptRes);
// 构建脚本资源
if (typeof code === "string") {
this.scriptFunc = compileScript(code);
} else {
this.scriptFunc = code;
}
const grantMap: { [key: string]: boolean } = {};
scriptRes.metadata.grant?.forEach((key) => {
grantMap[key] = true;
});
if (grantMap.none) {
// 不注入任何GM api
this.proxyContent = global;
} else {
// 构建脚本GM上下文
this.sandboxContent = createContext(scriptRes, this.GM_info, envPrefix, message);
this.proxyContent = proxyContext(global, this.sandboxContent, thisContext);
}
}
emitEvent(event: string, data: any) {
switch (event) {
case "menuClick":
this.sandboxContent?.menuClick(data);
break;
}
}
valueUpdate(data: ValueUpdateData) {
this.sandboxContent?.valueUpdate(data);
}
exec() {
this.logger.debug("script start");
return this.scriptFunc.apply(this.proxyContent, [this.proxyContent, this.GM_info]);
}
stop() {
this.logger.debug("script stop");
return true;
}
}

View File

@ -0,0 +1,82 @@
import { ScriptRunResouce } from "@App/app/repo/scripts";
import ExecScript from "./exec_script";
import { Message } from "@Packages/message/server";
export class CATRetryError {
msg: string;
time: Date;
constructor(msg: string, time: number | Date) {
this.msg = msg;
if (typeof time === "number") {
this.time = new Date(Date.now() + time * 1000);
} else {
this.time = time;
}
}
}
export class BgExecScriptWarp extends ExecScript {
setTimeout: Map<number, boolean>;
setInterval: Map<number, boolean>;
constructor(scriptRes: ScriptRunResouce, message: Message) {
const thisContext: { [key: string]: any } = {};
const setTimeout = new Map<number, any>();
const setInterval = new Map<number, any>();
thisContext.setTimeout = function (handler: () => void, timeout: number | undefined, ...args: any) {
const t = global.setTimeout(
function () {
setTimeout.delete(t);
if (typeof handler === "function") {
handler();
}
},
timeout,
...args
);
setTimeout.set(t, true);
return t;
};
thisContext.clearTimeout = function (t: number) {
setTimeout.delete(t);
global.clearTimeout(t);
};
thisContext.setInterval = function (handler: () => void, timeout: number | undefined, ...args: any) {
const t = global.setInterval(
function () {
if (typeof handler === "function") {
handler();
}
},
timeout,
...args
);
setInterval.set(t, true);
return t;
};
thisContext.clearInterval = function (t: number) {
setInterval.delete(t);
global.clearInterval(t);
};
// @ts-ignore
thisContext.CATRetryError = CATRetryError;
super(scriptRes, "offscreen", message, scriptRes.code, thisContext);
this.setTimeout = setTimeout;
this.setInterval = setInterval;
}
stop() {
this.setTimeout.forEach((_, t) => {
global.clearTimeout(t);
});
this.setTimeout.clear();
this.setInterval.forEach((_, t) => {
global.clearInterval(t);
});
this.setInterval.clear();
return super.stop();
}
}

View File

@ -0,0 +1,515 @@
import { ScriptRunResouce } from "@App/app/repo/scripts";
import { getMetadataStr, getUserConfigStr, parseUserConfig } from "@App/pkg/utils/script";
import { ValueUpdateData } from "./exec_script";
import { ExtVersion } from "@App/app/const";
import { Message, MessageConnect } from "@Packages/message/server";
import { CustomEventMessage } from "@Packages/message/custom_event_message";
import LoggerCore from "@App/app/logger/core";
import { connect, sendMessage } from "@Packages/message/client";
import EventEmitter from "eventemitter3";
import { getStorageName } from "@App/pkg/utils/utils";
interface ApiParam {
depend?: string[];
listener?: () => void;
}
export interface ApiValue {
api: any;
param: ApiParam;
}
export class GMContext {
static apis: Map<string, ApiValue> = new Map();
public static API(param: ApiParam = {}) {
return (target: any, propertyName: string, descriptor: PropertyDescriptor) => {
const key = propertyName;
if (param.listener) {
param.listener();
}
if (key === "GMdotXmlHttpRequest") {
GMContext.apis.set("GM.xmlHttpRequest", {
api: descriptor.value,
param,
});
return;
}
GMContext.apis.set(key, {
api: descriptor.value,
param,
});
// 兼容GM.*
const dot = key.replace("_", ".");
if (dot !== key) {
// 特殊处理GM.xmlHttpRequest
if (dot === "GM.xmlhttpRequest") {
return;
}
GMContext.apis.set(dot, {
api: descriptor.value,
param,
});
}
};
}
}
export default class GMApi {
scriptRes!: ScriptRunResouce;
runFlag!: string;
valueChangeListener = new Map<number, { name: string; listener: GMTypes.ValueChangeListener }>();
constructor(
private prefix: string,
private message: Message
) {}
// 单次回调使用
public sendMessage(api: string, params: any[]) {
return sendMessage(this.message, this.prefix + "/runtime/gmApi", {
uuid: this.scriptRes.uuid,
api,
params,
});
}
// 长连接使用,connect只用于接受消息,不发送消息
public connect(api: string, params: any[]) {
return connect(this.message, this.prefix + "/runtime/gmApi", {
uuid: this.scriptRes.uuid,
api,
params,
});
}
public valueUpdate(data: ValueUpdateData) {
if (data.uuid === this.scriptRes.uuid || data.storageName === getStorageName(this.scriptRes)) {
// 触发,并更新值
if (data.value === undefined) {
if (this.scriptRes.value[data.key] !== undefined) {
delete this.scriptRes.value[data.key];
}
} else {
this.scriptRes.value[data.key] = data.value;
}
this.valueChangeListener.forEach((item) => {
if (item.name === data.key) {
item.listener(data.key, data.oldValue, data.value, data.sender.runFlag !== this.runFlag, data.sender.tabId);
}
});
}
}
menuClick(id: number) {
this.EE.emit("menuClick" + id);
}
// 获取脚本信息和管理器信息
static GM_info(script: ScriptRunResouce) {
const metadataStr = getMetadataStr(script.code);
const userConfigStr = getUserConfigStr(script.code) || "";
const options = {
description: (script.metadata.description && script.metadata.description[0]) || null,
matches: script.metadata.match || [],
includes: script.metadata.include || [],
"run-at": (script.metadata["run-at"] && script.metadata["run-at"][0]) || "document-idle",
icon: (script.metadata.icon && script.metadata.icon[0]) || null,
icon64: (script.metadata.icon64 && script.metadata.icon64[0]) || null,
header: metadataStr,
grant: script.metadata.grant || [],
connects: script.metadata.connect || [],
};
return {
// downloadMode
// isIncognito
scriptWillUpdate: true,
scriptHandler: "ScriptCat",
scriptUpdateURL: script.downloadUrl,
scriptMetaStr: metadataStr,
userConfig: parseUserConfig(userConfigStr),
userConfigStr,
// scriptSource: script.sourceCode,
version: ExtVersion,
script: {
// TODO: 更多完整的信息(为了兼容Tampermonkey,后续待定)
name: script.name,
namespace: script.namespace,
version: script.metadata.version && script.metadata.version[0],
author: script.author,
...options,
},
};
}
// 获取脚本的值,可以通过@storageName让多个脚本共享一个储存空间
@GMContext.API()
public GM_getValue(key: string, defaultValue?: any) {
const ret = this.scriptRes.value[key];
if (ret) {
return ret;
}
return defaultValue;
}
@GMContext.API()
public GM_setValue(key: string, value: any) {
// 对object的value进行一次转化
if (typeof value === "object") {
value = JSON.parse(JSON.stringify(value));
}
if (value === undefined) {
delete this.scriptRes.value[key];
} else {
this.scriptRes.value[key] = value;
}
return this.sendMessage("GM_setValue", [key, value]);
}
@GMContext.API({ depend: ["GM_setValue"] })
public GM_deleteValue(name: string): void {
this.GM_setValue(name, undefined);
}
eventId: number = 0;
menuMap: Map<number, string> | undefined;
EE: EventEmitter = new EventEmitter();
@GMContext.API()
public GM_addValueChangeListener(name: string, listener: GMTypes.ValueChangeListener): number {
this.eventId += 1;
this.valueChangeListener.set(this.eventId, { name, listener });
return this.eventId;
}
@GMContext.API()
public GM_removeValueChangeListener(listenerId: number): void {
this.valueChangeListener.delete(listenerId);
}
@GMContext.API()
public GM_listValues(): string[] {
return Object.keys(this.scriptRes.value);
}
@GMContext.API()
GM_log(message: string, level?: GMTypes.LoggerLevel, labels?: GMTypes.LoggerLabel) {
if (typeof message !== "string") {
message = JSON.stringify(message);
}
return this.sendMessage("GM_log", [message, level, labels]);
}
@GMContext.API()
public CAT_createBlobUrl(blob: Blob): Promise<string> {
return this.sendMessage("CAT_createBlobUrl", [blob]);
}
// 辅助GM_xml获取blob数据
@GMContext.API()
public CAT_fetchBlob(url: string): Promise<Blob> {
return this.sendMessage("CAT_fetchBlob", [url]);
}
@GMContext.API()
public async CAT_fetchDocument(url: string): Promise<Document | undefined> {
const data = await this.sendMessage("CAT_fetchDocument", [url]);
return (<CustomEventMessage>this.message).getAndDelRelatedTarget(data.relatedTarget);
}
@GMContext.API()
GM_registerMenuCommand(name: string, listener: () => void, accessKey?: string): number {
if (!this.menuMap) {
this.menuMap = new Map();
}
let flag = 0;
this.menuMap.forEach((val, menuId) => {
if (val === name) {
flag = menuId;
}
});
if (flag) {
return flag;
}
this.eventId += 1;
const id = this.eventId;
this.menuMap.set(id, name);
this.EE.addListener("menuClick" + id, listener);
this.sendMessage("GM_registerMenuCommand", [id, name, accessKey]);
return id;
}
@GMContext.API()
GM_unregisterMenuCommand(id: number): void {
console.log("unregisterMenuCommand", id);
if (!this.menuMap) {
this.menuMap = new Map();
}
this.menuMap.delete(id);
this.EE.removeAllListeners("menuClick" + id);
this.sendMessage("GM_unregisterMenuCommand", [id]);
}
// 用于脚本跨域请求,需要@connect domain指定允许的域名
@GMContext.API({
depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"],
})
public GM_xmlhttpRequest(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,
maxRedirects: details.maxRedirects,
};
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<GMSend.XHRFormData> = [];
const keys: { [key: string]: boolean } = {};
details.data.forEach((val, key) => {
keys[key] = true;
});
// 处理FormData中的数据
await Promise.all(
Object.keys(keys).map((key) => {
const values = (<FormData>details.data).getAll(key);
return Promise.all(
values.map(async (val) => {
if (val instanceof File) {
const url = await this.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 this.CAT_createBlobUrl(details.data);
} else {
param.data = details.data;
}
// 处理返回数据
let readerStream: ReadableStream<Uint8Array> | undefined;
let controller: ReadableStreamDefaultController<Uint8Array> | 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<Uint8Array>({
start(ctrl) {
controller = ctrl;
},
});
}
return async (xhr: GMTypes.XHRResponse) => {
if (xhr.response) {
if (responseType === "document") {
xhr.response = await this.CAT_fetchDocument(<string>xhr.response);
xhr.responseXML = xhr.response;
xhr.responseType = "document";
} else {
const resp = await this.CAT_fetchBlob(<string>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);
}
}
}
// 发送信息
this.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", {
action: data.action,
});
break;
}
});
});
};
// 由于需要同步返回一个abort但是一些操作是异步的所以需要在这里处理
handler();
return {
abort: () => {
if (connect) {
connect.disconnect();
}
},
};
}
@GMContext.API()
public async GM_notification(
detail: GMTypes.NotificationDetails | string,
ondone?: GMTypes.NotificationOnDone | string,
image?: string,
onclick?: GMTypes.NotificationOnClick
) {
let data: GMTypes.NotificationDetails = {};
if (typeof detail === "string") {
data.text = detail;
switch (arguments.length) {
case 4:
data.onclick = onclick;
case 3:
data.image = image;
case 2:
data.title = <string>ondone;
default:
break;
}
} else {
data = detail;
data.ondone = data.ondone || <GMTypes.NotificationOnDone>ondone;
}
let click: GMTypes.NotificationOnClick;
let done: GMTypes.NotificationOnDone;
let create: GMTypes.NotificationOnClick;
if (data.onclick) {
click = data.onclick;
delete data.onclick;
}
if (data.ondone) {
done = data.ondone;
delete data.ondone;
}
if (data.oncreate) {
create = data.oncreate;
delete data.oncreate;
}
this.eventId += 1;
this.sendMessage("GM_notification", [data]);
this.EE.addListener("GM_notification:" + this.eventId, (resp: any) => {
switch (resp.event) {
case "click": {
click && click.apply({ id: resp.id }, [resp.id, resp.index]);
break;
}
case "done": {
done && done.apply({ id: resp.id }, [resp.user]);
break;
}
case "create": {
create && create.apply({ id: resp.id }, [resp.id]);
break;
}
default:
LoggerCore.logger().warn("GM_notification resp is error", {
resp,
});
break;
}
});
}
}

View File

@ -0,0 +1,65 @@
import { ScriptRunResouce } from "@App/app/repo/scripts";
import { Message, Server } from "@Packages/message/server";
import ExecScript, { ValueUpdateData } from "./exec_script";
import { addStyle, ScriptFunc } from "./utils";
import { getStorageName } from "@App/pkg/utils/utils";
import { EmitEventRequest } from "../service_worker/runtime";
export class InjectRuntime {
execList: ExecScript[] = [];
constructor(
private server: Server,
private msg: Message,
private scripts: ScriptRunResouce[]
) {}
start() {
this.scripts.forEach((script) => {
// @ts-ignore
const scriptFunc = window[script.flag];
if (scriptFunc) {
this.execScript(script, scriptFunc);
} else {
// 监听脚本加载,和屏蔽读取
Object.defineProperty(window, script.flag, {
configurable: true,
set: (val: ScriptFunc) => {
this.execScript(script, val);
},
});
}
});
this.server.on("runtime/emitEvent", (data: EmitEventRequest) => {
// 转发给脚本
const exec = this.execList.find((val) => val.scriptRes.uuid === data.uuid);
if (exec) {
exec.emitEvent(data.event, data.data);
}
});
this.server.on("runtime/valueUpdate", (data: ValueUpdateData) => {
this.execList
.filter((val) => val.scriptRes.uuid === data.uuid || getStorageName(val.scriptRes) === data.storageName)
.forEach((val) => {
val.valueUpdate(data);
});
});
}
execScript(script: ScriptRunResouce, scriptFunc: ScriptFunc) {
// @ts-ignore
delete window[script.flag];
const exec = new ExecScript(script, "content", this.msg, scriptFunc);
this.execList.push(exec);
// 注入css
if (script.metadata["require-css"]) {
script.metadata["require-css"].forEach((val) => {
const res = script.resource[val];
if (res) {
addStyle(res.content);
}
});
}
exec.exec();
}
}

View File

@ -0,0 +1,102 @@
import { describe, expect, it } from "vitest";
import { init, proxyContext, writables } from "./utils";
describe("proxy context", () => {
const context: any = {};
const global: any = {
gbok: "gbok",
onload: null,
eval: () => {
console.log("eval");
},
addEventListener: () => {},
removeEventListener: () => {},
location: "ok",
};
init.set("onload", true);
init.set("location", true);
const _this = proxyContext(global, context);
it("set contenxt", () => {
_this["md5"] = "ok";
expect(_this["md5"]).toEqual("ok");
expect(global["md5"]).toEqual(undefined);
});
it("set window null", () => {
_this["onload"] = "ok";
expect(_this["onload"]).toEqual("ok");
expect(global["onload"]).toEqual(null);
_this["onload"] = undefined;
expect(_this["onload"]).toEqual(undefined);
});
it("update", () => {
_this["okk"] = "ok";
expect(_this["okk"]).toEqual("ok");
expect(global["okk"]).toEqual(undefined);
_this["okk"] = "ok2";
expect(_this["okk"]).toEqual("ok2");
expect(global["okk"]).toEqual(undefined);
});
it("禁止穿透global对象", () => {
expect(_this["gbok"]).toBeUndefined();
});
it("禁止修改window", () => {
expect(() => (_this["window"] = "ok")).toThrow();
});
it("访问location", () => {
expect(_this.location).not.toBeUndefined();
});
});
// 只允许访问onxxxxx
describe("window", () => {
const _this = proxyContext({ onanimationstart: null }, {});
it("window", () => {
expect(_this.onanimationstart).toBeNull();
});
});
describe("兼容问题", () => {
const _this = proxyContext({}, {});
// https://github.com/xcanwin/KeepChatGPT 环境隔离得不够干净导致的
it("Uncaught TypeError: Illegal invocation #189", () => {
return new Promise((resolve) => {
console.log(_this.setTimeout.prototype);
_this.setTimeout(resolve, 100);
});
});
// AC-baidu-重定向优化百度搜狗谷歌必应搜索_favicon_双列
it("TypeError: Object.freeze is not a function #116", () => {
expect(() => _this.Object.freeze({})).not.toThrow();
});
});
describe("Symbol", () => {
const _this = proxyContext({}, {});
// 允许往global写入Symbol属性,影响内容: https://bbs.tampermonkey.net.cn/thread-5509-1-1.html
it("Symbol", () => {
const s = Symbol("test");
_this[s] = "ok";
expect(_this[s]).toEqual("ok");
});
// toString.call(window)返回的是'[object Object]'而不是'[object Window]',影响内容: https://github.com/scriptscat/scriptcat/issues/260
it("Window", () => {
expect(toString.call(_this)).toEqual("[object Window]");
});
});
// Object.hasOwnProperty穿透 https://github.com/scriptscat/scriptcat/issues/272
describe("Object", () => {
const _this = proxyContext({}, {});
it("hasOwnProperty", () => {
expect(_this.hasOwnProperty("test1")).toEqual(false);
_this.test1 = "ok";
expect(_this.hasOwnProperty("test1")).toEqual(true);
expect(_this.hasOwnProperty("test")).toEqual(true);
});
});

View File

@ -0,0 +1,355 @@
import { ScriptRunResouce } from "@App/app/repo/scripts";
import { v4 as uuidv4 } from "uuid";
import GMApi, { ApiValue, GMContext } from "./gm_api";
import { has } from "@App/pkg/utils/lodash";
import { Message } from "@Packages/message/server";
import EventEmitter from "eventemitter3";
// 构建脚本运行代码
export function compileScriptCode(scriptRes: ScriptRunResouce): string {
let require = "";
if (scriptRes.metadata.require) {
scriptRes.metadata.require.forEach((val) => {
const res = scriptRes.resource[val];
if (res) {
require = `${require}\n${res.content}`;
}
});
}
const code = require + scriptRes.code;
return `with (context) return (async ()=>{\n${code}\n//# sourceURL=${chrome.runtime.getURL(
`/${encodeURI(scriptRes.name)}.user.js`
)}\n})()`;
}
export type ScriptFunc = (context: any, GM_info: any) => any;
// 通过脚本代码编译脚本函数
export function compileScript(code: string): ScriptFunc {
return <ScriptFunc>new Function("context", "GM_info", code);
}
export function compileInjectScript(script: ScriptRunResouce): string {
return `window['${script.flag}']=function(context,GM_info){\n${script.code}\n}`;
}
// 设置api依赖
function setDepend(context: { [key: string]: any }, apiVal: ApiValue) {
if (apiVal.param.depend) {
for (let i = 0; i < apiVal.param.depend.length; i += 1) {
const value = apiVal.param.depend[i];
const dependApi = GMContext.apis.get(value);
if (!dependApi) {
return;
}
if (value.startsWith("GM.")) {
const [, t] = value.split(".");
(<{ [key: string]: any }>context.GM)[t] = dependApi.api.bind(context);
} else {
context[value] = dependApi.api.bind(context);
}
setDepend(context, dependApi);
}
}
}
// 构建沙盒上下文
export function createContext(scriptRes: ScriptRunResouce, GMInfo: any, envPrefix: string, message: Message): GMApi {
// 按照GMApi构建
const context: { [key: string]: any } = {
prefix: envPrefix,
message: message,
scriptRes,
valueChangeListener: new Map<number, { name: string; listener: GMTypes.ValueChangeListener }>(),
sendMessage: GMApi.prototype.sendMessage,
connect: GMApi.prototype.connect,
runFlag: uuidv4(),
eventId: 10000,
valueUpdate: GMApi.prototype.valueUpdate,
menuClick: GMApi.prototype.menuClick,
EE: new EventEmitter(),
GM: { Info: GMInfo },
GM_info: GMInfo,
};
if (scriptRes.metadata.grant) {
scriptRes.metadata.grant.forEach((val) => {
const api = GMContext.apis.get(val);
if (!api) {
return;
}
if (val.startsWith("GM.")) {
const [, t] = val.split(".");
(<{ [key: string]: any }>context.GM)[t] = api.api.bind(context);
} else if (val === "GM_cookie") {
// 特殊处理GM_cookie.list之类
context[val] = api.api.bind(context);
const GM_cookie = function (action: string) {
return (
details: GMTypes.CookieDetails,
done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void
) => {
return context[val](action, details, done);
};
};
context[val].list = GM_cookie("list");
context[val].delete = GM_cookie("delete");
context[val].set = GM_cookie("set");
} else {
context[val] = api.api.bind(context);
}
setDepend(context, api);
});
}
context.unsafeWindow = window;
return <GMApi>context;
}
export const writables: { [key: string]: any } = {
addEventListener: global.addEventListener.bind(global),
removeEventListener: global.removeEventListener.bind(global),
dispatchEvent: global.dispatchEvent.bind(global),
};
// 记录初始的window字段
export const init = new Map<string, boolean>();
// 需要用到全局的
export const unscopables: { [key: string]: boolean } = {
NodeFilter: true,
RegExp: true,
};
// 复制原有的,防止被前端网页复写
const descs = Object.getOwnPropertyDescriptors(global);
Object.keys(descs).forEach((key) => {
const desc = descs[key];
// 可写但不在特殊配置writables中
if (desc && desc.writable && !writables[key]) {
if (typeof desc.value === "function") {
// 判断是否需要bind例如Object、Function这些就不需要bind
if (desc.value.prototype) {
writables[key] = desc.value;
} else {
writables[key] = desc.value.bind(global);
}
} else {
writables[key] = desc.value;
}
} else {
init.set(key, true);
}
});
export function warpObject(thisContext: object, ...context: object[]) {
// 处理Object上的方法
thisContext.hasOwnProperty = (name: PropertyKey) => {
return (
Object.hasOwnProperty.call(thisContext, name) || context.some((val) => Object.hasOwnProperty.call(val, name))
);
};
thisContext.isPrototypeOf = (name: object) => {
return Object.isPrototypeOf.call(thisContext, name) || context.some((val) => Object.isPrototypeOf.call(val, name));
};
thisContext.propertyIsEnumerable = (name: PropertyKey) => {
return (
Object.propertyIsEnumerable.call(thisContext, name) ||
context.some((val) => Object.propertyIsEnumerable.call(val, name))
);
};
}
// 拦截上下文
export function proxyContext(global: any, context: any, thisContext?: { [key: string]: any }) {
const special = Object.assign(writables);
// 处理某些特殊的属性
// 后台脚本要不要考虑不能使用eval?
if (!thisContext) {
thisContext = {};
}
thisContext.eval = global.eval;
thisContext.define = undefined;
warpObject(thisContext, special, global, context);
// keyword是与createContext时同步的,避免访问到context的内部变量
const contextKeyword: { [key: string]: any } = {
message: 1,
valueChangeListener: 1,
connect: 1,
runFlag: 1,
valueUpdate: 1,
sendMessage: 1,
scriptRes: 1,
};
// @ts-ignore
const proxy = new Proxy(context, {
defineProperty(_, name, desc) {
if (Object.defineProperty(thisContext, name, desc)) {
return true;
}
return false;
},
get(_, name): any {
switch (name) {
case "window":
case "self":
case "globalThis":
return proxy;
case "top":
case "parent":
if (global[name] === global.self) {
return special.global || proxy;
}
return global.top;
default:
break;
}
if (name !== "undefined") {
if (has(thisContext, name)) {
// @ts-ignore
return thisContext[name];
}
if (typeof name === "string") {
if (has(context, name)) {
if (has(contextKeyword, name)) {
return undefined;
}
return context[name];
}
if (has(special, name)) {
if (typeof special[name] === "function" && !(<{ prototype: any }>special[name]).prototype) {
return (<{ bind: any }>special[name]).bind(global);
}
return special[name];
}
if (has(global, name)) {
// 特殊处理onxxxx的事件
if (name.startsWith("on")) {
if (typeof global[name] === "function" && !(<{ prototype: any }>global[name]).prototype) {
return (<{ bind: any }>global[name]).bind(global);
}
return global[name];
}
}
if (init.has(name)) {
const val = global[name];
if (typeof val === "function" && !(<{ prototype: any }>val).prototype) {
return (<{ bind: any }>val).bind(global);
}
return val;
}
} else if (name === Symbol.unscopables) {
return unscopables;
}
}
return undefined;
},
has(_, name) {
switch (name) {
case "window":
case "self":
case "globalThis":
return true;
case "top":
case "parent":
if (global[name] === global.self) {
return true;
}
return true;
default:
break;
}
if (name !== "undefined") {
if (typeof name === "string") {
if (has(unscopables, name)) {
return false;
}
if (has(thisContext, name)) {
return true;
}
if (has(context, name)) {
if (has(contextKeyword, name)) {
return false;
}
return true;
}
if (has(special, name)) {
return true;
}
// 只处理onxxxx的事件
if (has(global[name], name)) {
if (name.startsWith("on")) {
return true;
}
}
} else if (typeof name === "symbol") {
return has(thisContext, name);
}
}
return false;
},
set(_, name: string, val) {
switch (name) {
case "window":
case "self":
case "globalThis":
return false;
default:
}
if (has(special, name)) {
special[name] = val;
return true;
}
if (init.has(name)) {
const des = Object.getOwnPropertyDescriptor(global, name);
// 只读的return
if (des && des.get && !des.set && des.configurable) {
return true;
}
// 只处理onxxxx的事件
if (has(global, name) && name.startsWith("on")) {
if (val === undefined) {
global.removeEventListener(name.slice(2), thisContext[name]);
} else {
if (thisContext[name]) {
global.removeEventListener(name.slice(2), thisContext[name]);
}
global.addEventListener(name.slice(2), val);
}
thisContext[name] = val;
return true;
}
}
// @ts-ignore
thisContext[name] = val;
return true;
},
getOwnPropertyDescriptor(_, name) {
try {
let ret = Object.getOwnPropertyDescriptor(thisContext, name);
if (ret) {
return ret;
}
ret = Object.getOwnPropertyDescriptor(context, name);
if (ret) {
return ret;
}
ret = Object.getOwnPropertyDescriptor(global, name);
return ret;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return undefined;
}
},
});
proxy[Symbol.toStringTag] = "Window";
return proxy;
}
export function addStyle(css: string): HTMLElement {
const dom = document.createElement("style");
dom.innerHTML = css;
if (document.head) {
return document.head.appendChild(dom);
}
return document.documentElement.appendChild(dom);
}

View File

@ -0,0 +1,16 @@
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
import { initTestEnv } from "@Tests/utils";
initTestEnv();
// serviceWorker环境
beforeAll(() => {});
describe("GM xhr", () => {
beforeEach(() => {
// See https://webext-core.aklinker1.io/fake-browser/reseting-state
});
it("123123", async () => {
expect(1).toBe(1);
});
});

View File

@ -48,7 +48,9 @@ export class OffscreenManager {
script.init();
// 转发从sandbox来的gm api请求
forwardMessage("serviceWorker", "runtime/gmApi", this.windowServer, this.extensionMessage);
// 转发message queue请求
// 转发valueUpdate与emitEvent
forwardMessage("sandbox", "runtime/valueUpdate", this.windowServer, this.windowMessage);
forwardMessage("sandbox", "runtime/emitEvent", this.windowServer, this.windowMessage);
const gmApi = new GMApi(this.windowServer.group("gmApi"));
gmApi.init();

View File

@ -7,12 +7,14 @@ import {
SCRIPT_TYPE_BACKGROUND,
ScriptRunResouce,
} from "@App/app/repo/scripts";
import ExecScript from "@App/runtime/content/exec_script";
import { BgExecScriptWarp, CATRetryError } from "@App/runtime/content/exec_warp";
import { Server } from "@Packages/message/server";
import { WindowMessage } from "@Packages/message/window_message";
import { CronJob } from "cron";
import { proxyUpdateRunStatus } from "../offscreen/client";
import { BgExecScriptWarp } from "../content/exec_warp";
import ExecScript, { ValueUpdateData } from "../content/exec_script";
import { getStorageName } from "@App/pkg/utils/utils";
import { EmitEventRequest } from "../service_worker/runtime";
export class Runtime {
cronJob: Map<string, Array<CronJob>> = new Map();
@ -290,10 +292,30 @@ export class Runtime {
return this.execScript(script, true);
}
valueUpdate(data: ValueUpdateData) {
// 转发给脚本
this.execScripts.forEach((val) => {
if (val.scriptRes.uuid === data.uuid || getStorageName(val.scriptRes) === data.storageName) {
val.valueUpdate(data);
}
});
}
emitEvent(data: EmitEventRequest) {
// 转发给脚本
const exec = this.execScripts.get(data.uuid);
if (exec) {
exec.emitEvent(data.event, data.data);
}
}
init() {
this.api.on("enableScript", this.enableScript.bind(this));
this.api.on("disableScript", this.disableScript.bind(this));
this.api.on("runScript", this.runScript.bind(this));
this.api.on("stopScript", this.stopScript.bind(this));
this.api.on("runtime/valueUpdate", this.valueUpdate.bind(this));
this.api.on("runtime/emitEvent", this.emitEvent.bind(this));
}
}

View File

@ -6,7 +6,6 @@ import { ValueService } from "@App/app/service/service_worker/value";
import PermissionVerify from "./permission_verify";
import { connect } from "@Packages/message/client";
import Cache, { incr } from "@App/app/cache";
import { unsafeHeaders } from "@App/runtime/utils";
import EventEmitter from "eventemitter3";
import { MessageQueue } from "@Packages/message/message_queue";
import { RuntimeService } from "./runtime";
@ -24,6 +23,35 @@ export type Request = MessageRequest & {
script: Script;
};
export const unsafeHeaders: { [key: string]: boolean } = {
// 部分浏览器中并未允许
"user-agent": true,
// 这两个是前缀
"proxy-": true,
"sec-": true,
// cookie已经特殊处理
cookie: true,
"accept-charset": true,
"accept-encoding": true,
"access-control-request-headers": true,
"access-control-request-method": true,
connection: true,
"content-length": true,
date: true,
dnt: true,
expect: true,
"feature-policy": true,
host: true,
"keep-alive": true,
origin: true,
referer: true,
te: true,
trailer: true,
"transfer-encoding": true,
upgrade: true,
via: true,
};
export type Api = (request: Request, con: GetSender) => Promise<any>;
export default class GMApi {
@ -194,7 +222,7 @@ export default class GMApi {
id: id,
name: name,
accessKey: accessKey,
tabId: sender.getSender().tab!.id!,
tabId: sender.getSender().tab?.id || -1,
frameId: sender.getSender().frameId,
documentId: sender.getSender().documentId,
});
@ -207,7 +235,7 @@ export default class GMApi {
this.mq.emit("unregisterMenuCommand", {
uuid: request.script.uuid,
id: id,
tabId: sender.getSender().tab!.id!,
tabId: sender.getSender().tab?.id || -1,
frameId: sender.getSender().frameId,
});
}

View File

@ -20,7 +20,7 @@ import {
subscribeScriptMenuRegister,
subscribeScriptRunStatus,
} from "../queue";
import { getStorageName } from "@App/runtime/utils";
import { getStorageName } from "@App/pkg/utils/utils";
export type ScriptMenuItem = {
id: number;
@ -206,7 +206,7 @@ export class PopupService {
// 事务更新脚本菜单
txUpdateScriptMenu(tabId: number, callback: (menu: ScriptMenu[]) => Promise<any>) {
return Cache.getInstance().tx("tabScript:" + tabId, async (menu) => {
return Cache.getInstance().tx<ScriptMenu[]>("tabScript:" + tabId, async (menu) => {
return callback(menu || []);
});
}
@ -252,14 +252,15 @@ export class PopupService {
if (script.type === SCRIPT_TYPE_NORMAL) {
return;
}
if (script.status !== SCRIPT_STATUS_ENABLE) {
return;
}
return this.txUpdateScriptMenu(-1, async (menu) => {
const scriptMenu = menu.find((item) => item.uuid === script.uuid);
if (script.status === SCRIPT_STATUS_ENABLE) {
// 加入菜单
if (!scriptMenu) {
const item = this.scriptToMenu(script);
menu.push(item);
}
// 加入菜单
if (!scriptMenu) {
const item = this.scriptToMenu(script);
menu.push(item);
}
return menu;
});
@ -294,24 +295,22 @@ export class PopupService {
const index = menu.findIndex((item) => item.uuid === uuid);
if (index !== -1) {
menu.splice(index, 1);
return menu;
}
return null;
return menu;
});
});
subscribeScriptRunStatus(this.mq, async ({ uuid, runStatus }) => {
return this.txUpdateScriptMenu(-1, async (menu) => {
const scriptMenu = menu.find((item) => item.uuid === uuid);
if (scriptMenu) {
if (scriptMenu.runStatus === SCRIPT_RUN_STATUS_RUNNING) {
scriptMenu.runStatus = runStatus;
if (runStatus === SCRIPT_RUN_STATUS_RUNNING) {
scriptMenu.runNum = 1;
} else {
scriptMenu.runNum = 0;
}
scriptMenu.runStatus = runStatus;
return menu;
}
return null;
return menu;
});
});
}
@ -330,13 +329,12 @@ export class PopupService {
documentId: string;
}) {
// 菜单点击事件
this.runtime.sendMessageToTab(
this.runtime.EmitEventToTab(
tabId,
"menuClick",
{
uuid,
id,
tabId,
event: "menuClick",
data: id,
},
{
frameId,
@ -372,14 +370,21 @@ export class PopupService {
const [, , uuid, id] = menuIds;
// 寻找menu信息
const menu = await this.getScriptMenu(tab!.id!);
const script = menu.find((item) => item.uuid === uuid);
let script = menu.find((item) => item.uuid === uuid);
let bgscript = false;
if (!script) {
// 从后台脚本中寻找
const backgroundMenu = await this.getScriptMenu(-1);
script = backgroundMenu.find((item) => item.uuid === uuid);
bgscript = true;
}
if (script) {
const menuItem = script.menus.find((item) => item.id === parseInt(id, 10));
if (menuItem) {
this.menuClick({
uuid: script.uuid,
id: menuItem.id,
tabId: tab!.id!,
tabId: bgscript ? -1 : tab!.id!,
frameId: menuItem.frameId || 0,
documentId: menuItem.documentId || "",
});

View File

@ -16,11 +16,11 @@ import { ScriptService } from "./script";
import { runScript, stopScript } from "../offscreen/client";
import { getRunAt } from "./utils";
import { randomString } from "@App/pkg/utils/utils";
import { compileInjectScript } from "@App/runtime/content/utils";
import Cache from "@App/app/cache";
import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match";
import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
import { sendMessage } from "@Packages/message/client";
import { compileInjectScript } from "../content/utils";
// 为了优化性能存储到缓存时删除了code与value
export interface ScriptMatchInfo extends ScriptRunResouce {
@ -29,6 +29,12 @@ export interface ScriptMatchInfo extends ScriptRunResouce {
customizeExcludeMatches: string[];
}
export interface EmitEventRequest {
uuid: string;
event: string;
data: any;
}
export class RuntimeService {
scriptDAO: ScriptDAO = new ScriptDAO();
@ -132,6 +138,22 @@ export class RuntimeService {
return sendMessage(new ExtensionContentMessageSend(tabId, options), "content/runtime/" + action, data);
}
// 给指定脚本触发事件
EmitEventToTab(
tabId: number,
req: EmitEventRequest,
options?: {
documentId?: string;
frameId?: number;
}
) {
if (tabId === -1) {
// 如果是-1, 代表给offscreen发送消息
return sendMessage(this.sender, "offscreen/runtime/emitEvent", req);
}
return sendMessage(new ExtensionContentMessageSend(tabId, options), "content/runtime/emitEvent", req);
}
async getPageScriptUuidByUrl(url: string, includeCustomize?: boolean) {
const match = await this.loadScriptMatchInfo();
// 匹配当前页面的脚本

View File

@ -19,7 +19,7 @@ import { MessageQueue } from "@Packages/message/message_queue";
import { InstallSource } from ".";
import { ResourceService } from "./resource";
import { ValueService } from "./value";
import { compileScriptCode } from "@App/runtime/content/utils";
import { compileScriptCode } from "../content/utils";
export class ScriptService {
logger: Logger;

View File

@ -2,13 +2,13 @@ import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { Script, SCRIPT_TYPE_NORMAL, ScriptDAO } from "@App/app/repo/scripts";
import { ValueDAO } from "@App/app/repo/value";
import { getStorageName } from "@App/runtime/utils";
import { Group, MessageSend } from "@Packages/message/server";
import { RuntimeService } from "./runtime";
import { PopupService } from "./popup";
import { ValueUpdateData, ValueUpdateSender } from "@App/runtime/content/exec_script";
import { sendMessage } from "@Packages/message/client";
import Cache from "@App/app/cache";
import { getStorageName } from "@App/pkg/utils/utils";
import { ValueUpdateData, ValueUpdateSender } from "../content/exec_script";
export class ValueService {
logger: Logger;
@ -67,21 +67,18 @@ export class ValueService {
uuid,
storageName: storageName,
};
// 判断是后台脚本还是前台脚本
if (script.type === SCRIPT_TYPE_NORMAL) {
chrome.tabs.query({}, (tabs) => {
// 推送到所有加载了本脚本的tab中
tabs.forEach(async (tab) => {
const scriptMenu = await this.popup!.getScriptMenu(tab.id!);
if (scriptMenu.find((item) => item.storageName === storageName)) {
this.runtime!.sendMessageToTab(tab.id!, "valueUpdate", sendData);
}
});
chrome.tabs.query({}, (tabs) => {
// 推送到所有加载了本脚本的tab中
tabs.forEach(async (tab) => {
const scriptMenu = await this.popup!.getScriptMenu(tab.id!);
if (scriptMenu.find((item) => item.storageName === storageName)) {
this.runtime!.sendMessageToTab(tab.id!, "valueUpdate", sendData);
}
});
} else {
// 推送到offscreen中
sendMessage(this.send, "offscreen/runtime/valueUpdate", sendData);
}
});
// 推送到offscreen中
sendMessage(this.send, "offscreen/runtime/valueUpdate", sendData);
return Promise.resolve(true);
}