王一之 131f1bda40
Some checks failed
test / Run tests (push) Failing after 15s
build / Build (push) Failing after 22s
单测
2025-03-20 23:34:38 +08:00

265 lines
7.4 KiB
TypeScript

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 { storageKey } from "../utils";
import { Message, MessageConnect } from "@Packages/message/server";
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 message: Message) {}
// 单次回调使用
public sendMessage(api: string, params: any[]) {
return this.message.sendMessage({
action: "runtime/gmApi",
data: {
uuid: this.scriptRes.uuid,
api,
params,
},
});
}
// 长连接使用,connect只用于接受消息,不发送消息
public connect(api: string, params: any[]) {
return this.message.connect({
action: "runtime/gmApi",
data: {
uuid: this.scriptRes.uuid,
api,
params,
},
});
}
public valueUpdate(data: ValueUpdateData) {
if (data.uuid === this.scriptRes.uuid || data.storageKey === storageKey(this.scriptRes)) {
// 触发,并更新值
if (data.value === undefined) {
delete this.scriptRes.value[data.value];
} else {
this.scriptRes.value[data.key] = data.value;
}
this.valueChangeListener.forEach((item) => {
if (item.name === data.value.key) {
item.listener(
data.value.key,
data.oldValue,
data.value,
data.sender.runFlag !== this.runFlag,
data.sender.tabId
);
}
});
}
}
// 获取脚本信息和管理器信息
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);
}
@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]);
}
// 用于脚本跨域请求,需要@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;
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(resp.data));
break;
}
});
});
return {
abort: () => {
if (connect) {
connect.disconnect();
}
},
};
}
}