gm api通信
Some checks failed
test / Run tests (push) Failing after 9s
build / Build (push) Failing after 16s
Some checks failed
test / Run tests (push) Failing after 9s
build / Build (push) Failing after 16s
This commit is contained in:
parent
415f00a3d1
commit
9f8f7c8347
@ -1,32 +1,24 @@
|
||||
export function sendMessage(action: string, data?: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ action, data }, (res) => {
|
||||
if (res.code) {
|
||||
console.error(res);
|
||||
reject(res.message);
|
||||
} else {
|
||||
resolve(res.data);
|
||||
}
|
||||
});
|
||||
});
|
||||
import { Message, MessageConnect } from "./server";
|
||||
|
||||
export function sendMessage(msg: Message, action: string, data?: any): Promise<any> {
|
||||
return msg.sendMessage({ action, data });
|
||||
}
|
||||
|
||||
export function connect(action: string, data?: any): Promise<chrome.runtime.Port> {
|
||||
return new Promise((resolve) => {
|
||||
const port = chrome.runtime.connect();
|
||||
port.postMessage({ action, data });
|
||||
resolve(port);
|
||||
});
|
||||
export function connect(msg: Message, action: string, data?: any): Promise<MessageConnect> {
|
||||
return msg.connect({ action, data });
|
||||
}
|
||||
|
||||
export class Client {
|
||||
constructor(private prefix: string) {
|
||||
constructor(
|
||||
private msg: Message,
|
||||
private prefix: string
|
||||
) {
|
||||
if (!this.prefix.endsWith("/")) {
|
||||
this.prefix += "/";
|
||||
}
|
||||
}
|
||||
|
||||
do(action: string, params?: any): Promise<any> {
|
||||
return sendMessage(this.prefix + action, params);
|
||||
return sendMessage(this.msg, this.prefix + action, params);
|
||||
}
|
||||
}
|
||||
|
@ -87,3 +87,28 @@ export class Group {
|
||||
this.server.on(`${this.name}${name}`, func);
|
||||
}
|
||||
}
|
||||
|
||||
// 转发消息
|
||||
export function forwardMessage(path: string, from: Server, to: Message) {
|
||||
from.on(path, (params, fromCon) => {
|
||||
console.log(params, fromCon);
|
||||
if (fromCon) {
|
||||
to.connect({ action: path, data: params }).then((toCon) => {
|
||||
fromCon.onMessage((data) => {
|
||||
toCon.sendMessage(data);
|
||||
});
|
||||
toCon.onMessage((data) => {
|
||||
fromCon.sendMessage(data);
|
||||
});
|
||||
fromCon.onDisconnect(() => {
|
||||
toCon.disconnect();
|
||||
});
|
||||
toCon.onDisconnect(() => {
|
||||
fromCon.disconnect();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return to.sendMessage({ action: path, data: params });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -77,6 +77,7 @@ export default defineConfig({
|
||||
parser: {
|
||||
syntax: "typescript",
|
||||
tsx: true,
|
||||
decorators: true,
|
||||
},
|
||||
transform: {
|
||||
react: {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Repo } from "./repo";
|
||||
import { Resource } from "./resource";
|
||||
import { Value } from "./value";
|
||||
|
||||
// 脚本模型
|
||||
export type SCRIPT_TYPE = 1 | 2 | 3;
|
||||
@ -71,15 +70,19 @@ export interface ScriptCode {
|
||||
code: string; // 脚本执行代码
|
||||
}
|
||||
|
||||
export type ScriptAndCode = Script & ScriptCode;
|
||||
|
||||
// 脚本运行时的资源,包含已经编译好的脚本与脚本需要的资源
|
||||
export interface ScriptRunResouce extends Script {
|
||||
code: string;
|
||||
value: { [key: string]: Value };
|
||||
value: { [key: string]: any };
|
||||
flag: string;
|
||||
resource: { [key: string]: Resource };
|
||||
}
|
||||
|
||||
export class ScriptDAO extends Repo<Script> {
|
||||
scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO();
|
||||
|
||||
constructor() {
|
||||
super("script");
|
||||
}
|
||||
@ -88,6 +91,15 @@ export class ScriptDAO extends Repo<Script> {
|
||||
return super._save(val.uuid, val);
|
||||
}
|
||||
|
||||
getAndCode(uuid: string): Promise<ScriptAndCode|undefined> {
|
||||
return Promise.all([this.get(uuid), this.scriptCodeDAO.get(uuid)]).then(([script, code]) => {
|
||||
if (!script || !code) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.assign(script, code);
|
||||
});
|
||||
}
|
||||
|
||||
public findByName(name: string) {
|
||||
return this.findOne((key, value) => {
|
||||
return value.name === name;
|
||||
@ -100,10 +112,6 @@ export class ScriptDAO extends Repo<Script> {
|
||||
});
|
||||
}
|
||||
|
||||
public findByUUID(uuid: string) {
|
||||
return this.get(uuid);
|
||||
}
|
||||
|
||||
public findByUUIDAndSubscribeUrl(uuid: string, suburl: string) {
|
||||
return this.findOne((key, value) => {
|
||||
return value.uuid === uuid && value.subscribeUrl === suburl;
|
||||
|
@ -1,15 +1,5 @@
|
||||
import { Repo } from "./repo";
|
||||
|
||||
export interface OldValue {
|
||||
id: number;
|
||||
scriptId: number;
|
||||
storageName?: string;
|
||||
key: string;
|
||||
value: any;
|
||||
createtime: number;
|
||||
updatetime: number;
|
||||
}
|
||||
|
||||
export interface Value {
|
||||
uuid: string;
|
||||
storageName?: string;
|
||||
@ -22,4 +12,8 @@ export class ValueDAO extends Repo<Value> {
|
||||
constructor() {
|
||||
super("value");
|
||||
}
|
||||
|
||||
save(key: string, value: Value) {
|
||||
return super._save(key, value);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { WindowMessage } from "@Packages/message/window_message";
|
||||
import { sendMessage } from "../utils";
|
||||
import { SCRIPT_RUN_STATUS } from "@App/app/repo/scripts";
|
||||
import { sendMessage } from "@Packages/message/client";
|
||||
|
||||
export function preparationSandbox(msg: WindowMessage) {
|
||||
return sendMessage(msg, "preparationSandbox");
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Server } from "@Packages/message/server";
|
||||
import { forwardMessage, Server } from "@Packages/message/server";
|
||||
import { ScriptService } from "./script";
|
||||
import { Broker, MessageQueue } from "@Packages/message/message_queue";
|
||||
import { Logger, LoggerDAO } from "@App/app/repo/logger";
|
||||
@ -21,6 +21,8 @@ export class OffscreenManager {
|
||||
|
||||
private broker: Broker = new Broker(this.extensionMessage);
|
||||
|
||||
private serviceWorker = new ServiceWorkerClient(this.extensionMessage);
|
||||
|
||||
logger(data: Logger) {
|
||||
const dao = new LoggerDAO();
|
||||
dao.save(data);
|
||||
@ -28,12 +30,11 @@ export class OffscreenManager {
|
||||
|
||||
preparationSandbox() {
|
||||
// 通知初始化好环境了
|
||||
const serviceWorker = new ServiceWorkerClient();
|
||||
serviceWorker.preparationOffscreen();
|
||||
this.serviceWorker.preparationOffscreen();
|
||||
}
|
||||
|
||||
sendMessageToServiceWorker(data: { action: string; data: any }) {
|
||||
return sendMessage(data.action, data.data);
|
||||
return sendMessage(this.extensionMessage, data.action, data.data);
|
||||
}
|
||||
|
||||
initManager() {
|
||||
@ -44,5 +45,7 @@ export class OffscreenManager {
|
||||
this.windowApi.on("sendMessageToServiceWorker", this.sendMessageToServiceWorker.bind(this));
|
||||
const script = new ScriptService(group.group("script"), this.mq, this.windowMessage, this.broker);
|
||||
script.init();
|
||||
// 转发gm api请求
|
||||
forwardMessage("serviceWorker/runtime/gmApi", this.windowApi, this.extensionMessage);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||
import { sendMessage } from "@Packages/message/client";
|
||||
import { WindowMessage } from "@Packages/message/window_message";
|
||||
import { sendMessage } from "../utils";
|
||||
|
||||
export function enableScript(msg: WindowMessage, data: ScriptRunResouce) {
|
||||
return sendMessage(msg, "enableScript", data);
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { Server } from "@Packages/message/server";
|
||||
import { WindowMessage } from "@Packages/message/window_message";
|
||||
import { preparationSandbox } from "../offscreen/client";
|
||||
import { Script, SCRIPT_TYPE_BACKGROUND } from "@App/app/repo/scripts";
|
||||
import { CronJob } from "cron";
|
||||
import ExecScript from "@App/runtime/content/exec_script";
|
||||
import { Runtime } from "./runtime";
|
||||
|
||||
// sandbox环境的管理器
|
||||
|
@ -107,7 +107,7 @@ export class Runtime {
|
||||
// 暂未实现执行完成后立马释放,会在下一次执行时释放
|
||||
await this.stopScript(script.uuid);
|
||||
}
|
||||
const exec = new BgExecScriptWarp(script);
|
||||
const exec = new BgExecScriptWarp(script, this.windowMessage);
|
||||
this.execScripts.set(script.uuid, exec);
|
||||
proxyUpdateRunStatus(this.windowMessage, { uuid: script.uuid, runStatus: SCRIPT_RUN_STATUS_RUNNING });
|
||||
// 修改掉脚本掉最后运行时间, 数据库也需要修改
|
||||
|
@ -3,10 +3,11 @@ import { Client } from "@Packages/message/client";
|
||||
import { InstallSource } from ".";
|
||||
import { Broker } from "@Packages/message/message_queue";
|
||||
import { Resource } from "@App/app/repo/resource";
|
||||
import { Message } from "@Packages/message/server";
|
||||
|
||||
export class ServiceWorkerClient extends Client {
|
||||
constructor() {
|
||||
super("serviceWorker");
|
||||
constructor(msg: Message) {
|
||||
super(msg, "serviceWorker");
|
||||
}
|
||||
|
||||
preparationOffscreen() {
|
||||
@ -15,8 +16,8 @@ export class ServiceWorkerClient extends Client {
|
||||
}
|
||||
|
||||
export class ScriptClient extends Client {
|
||||
constructor() {
|
||||
super("serviceWorker/script");
|
||||
constructor(msg: Message) {
|
||||
super(msg, "serviceWorker/script");
|
||||
}
|
||||
|
||||
// 获取安装信息
|
||||
@ -50,8 +51,8 @@ export class ScriptClient extends Client {
|
||||
}
|
||||
|
||||
export class ResourceClient extends Client {
|
||||
constructor() {
|
||||
super("serviceWorker/resource");
|
||||
constructor(msg: Message) {
|
||||
super(msg, "serviceWorker/resource");
|
||||
}
|
||||
|
||||
getScriptResources(script: Script): Promise<{ [key: string]: Resource }> {
|
||||
@ -60,8 +61,8 @@ export class ResourceClient extends Client {
|
||||
}
|
||||
|
||||
export class ValueClient extends Client {
|
||||
constructor() {
|
||||
super("serviceWorker/value");
|
||||
constructor(msg: Message) {
|
||||
super(msg, "serviceWorker/value");
|
||||
}
|
||||
|
||||
getScriptValue(script: Script) {
|
||||
|
@ -29,7 +29,7 @@ export default class ServiceWorkerManager {
|
||||
value.init();
|
||||
const script = new ScriptService(group.group("script"), this.mq, value, resource);
|
||||
script.init();
|
||||
const runtime = new RuntimeService(group.group("runtime"), this.mq);
|
||||
const runtime = new RuntimeService(group.group("runtime"), this.mq, value);
|
||||
runtime.init();
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,23 @@
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { ScriptEnableCallbackValue } from "./client";
|
||||
import { Group } from "@Packages/message/server";
|
||||
import { Script, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL, ScriptDAO } from "@App/app/repo/scripts";
|
||||
import { Script, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL, ScriptAndCode, ScriptDAO } from "@App/app/repo/scripts";
|
||||
import GMApi from "@App/runtime/service_worker/gm_api";
|
||||
import { ValueService } from "./value";
|
||||
|
||||
export class RuntimeService {
|
||||
scriptDAO: ScriptDAO = new ScriptDAO();
|
||||
|
||||
constructor(
|
||||
private group: Group,
|
||||
private mq: MessageQueue
|
||||
private mq: MessageQueue,
|
||||
private value: ValueService
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
// 监听脚本开启
|
||||
this.mq.addListener("enableScript", async (data: ScriptEnableCallbackValue) => {
|
||||
const script = await this.scriptDAO.get(data.uuid);
|
||||
const script = await this.scriptDAO.getAndCode(data.uuid);
|
||||
if (!script) {
|
||||
return;
|
||||
}
|
||||
@ -48,9 +51,17 @@ export class RuntimeService {
|
||||
this.mq.publish("enableScript", { uuid: script.uuid, enable: true });
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化gm api
|
||||
const gmApi = new GMApi(this.value);
|
||||
gmApi.start();
|
||||
// 处理请求
|
||||
this.group.on("gmApi", gmApi.handlerRequest);
|
||||
}
|
||||
|
||||
registryPageScript(script: Script) {}
|
||||
registryPageScript(script: ScriptAndCode) {
|
||||
console.log(script);
|
||||
}
|
||||
|
||||
unregistryPageScript(script: Script) {}
|
||||
}
|
||||
|
@ -11,14 +11,12 @@ import {
|
||||
SCRIPT_RUN_STATUS,
|
||||
SCRIPT_STATUS_DISABLE,
|
||||
SCRIPT_STATUS_ENABLE,
|
||||
SCRIPT_TYPE_NORMAL,
|
||||
ScriptCodeDAO,
|
||||
ScriptDAO,
|
||||
ScriptRunResouce,
|
||||
} from "@App/app/repo/scripts";
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { InstallSource } from ".";
|
||||
import { ScriptEnableCallbackValue } from "./client";
|
||||
import { ResourceService } from "./resource";
|
||||
import { ValueService } from "./value";
|
||||
import { compileScriptCode } from "@App/runtime/content/utils";
|
||||
@ -163,23 +161,26 @@ export class ScriptService {
|
||||
upsertBy,
|
||||
});
|
||||
let update = false;
|
||||
const dao = new ScriptDAO();
|
||||
// 判断是否已经安装
|
||||
const oldScript = await dao.findByUUID(script.uuid);
|
||||
const oldScript = await this.scriptDAO.get(script.uuid);
|
||||
if (oldScript) {
|
||||
// 执行更新逻辑
|
||||
update = true;
|
||||
script.selfMetadata = oldScript.selfMetadata;
|
||||
}
|
||||
return dao
|
||||
return this.scriptDAO
|
||||
.save(script)
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
await this.scriptCodeDAO.save({
|
||||
uuid: script.uuid,
|
||||
code: param.code,
|
||||
});
|
||||
logger.info("install success");
|
||||
// 广播一下
|
||||
this.mq.publish("installScript", { script, update });
|
||||
return {};
|
||||
})
|
||||
.catch((e) => {
|
||||
.catch((e: any) => {
|
||||
logger.error("install error", Logger.E(e));
|
||||
throw e;
|
||||
});
|
||||
@ -187,13 +188,12 @@ export class ScriptService {
|
||||
|
||||
async deleteScript(uuid: string) {
|
||||
const logger = this.logger.with({ uuid });
|
||||
const dao = new ScriptDAO();
|
||||
const script = await dao.findByUUID(uuid);
|
||||
const script = await this.scriptDAO.get(uuid);
|
||||
if (!script) {
|
||||
logger.error("script not found");
|
||||
throw new Error("script not found");
|
||||
}
|
||||
return dao
|
||||
return this.scriptDAO
|
||||
.delete(uuid)
|
||||
.then(() => {
|
||||
logger.info("delete success");
|
||||
@ -208,13 +208,12 @@ export class ScriptService {
|
||||
|
||||
async enableScript(param: { uuid: string; enable: boolean }) {
|
||||
const logger = this.logger.with({ uuid: param.uuid, enable: param.enable });
|
||||
const dao = new ScriptDAO();
|
||||
const script = await dao.findByUUID(param.uuid);
|
||||
const script = await this.scriptDAO.get(param.uuid);
|
||||
if (!script) {
|
||||
logger.error("script not found");
|
||||
throw new Error("script not found");
|
||||
}
|
||||
return dao
|
||||
return this.scriptDAO
|
||||
.update(param.uuid, { status: param.enable ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE })
|
||||
.then(() => {
|
||||
logger.info("enable success");
|
||||
@ -228,7 +227,7 @@ export class ScriptService {
|
||||
}
|
||||
|
||||
async fetchInfo(uuid: string) {
|
||||
const script = await new ScriptDAO().findByUUID(uuid);
|
||||
const script = await this.scriptDAO.get(uuid);
|
||||
if (!script) {
|
||||
return null;
|
||||
}
|
||||
@ -236,7 +235,7 @@ export class ScriptService {
|
||||
}
|
||||
|
||||
async updateRunStatus(params: { uuid: string; runStatus: SCRIPT_RUN_STATUS; error?: string; nextruntime?: number }) {
|
||||
await new ScriptDAO().update(params.uuid, {
|
||||
await this.scriptDAO.update(params.uuid, {
|
||||
runStatus: params.runStatus,
|
||||
lastruntime: new Date().getTime(),
|
||||
error: params.error,
|
||||
|
@ -1,12 +1,14 @@
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import { Script } from "@App/app/repo/scripts";
|
||||
import { Script, ScriptDAO } from "@App/app/repo/scripts";
|
||||
import { ValueDAO } from "@App/app/repo/value";
|
||||
import { storageKey } from "@App/runtime/utils";
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { Group } from "@Packages/message/server";
|
||||
|
||||
export class ValueService {
|
||||
logger: Logger;
|
||||
scriptDAO: ScriptDAO = new ScriptDAO();
|
||||
valueDAO: ValueDAO = new ValueDAO();
|
||||
|
||||
constructor(
|
||||
@ -16,21 +18,37 @@ export class ValueService {
|
||||
this.logger = LoggerCore.logger().with({ service: "value" });
|
||||
}
|
||||
|
||||
storageKey(script: Script): string {
|
||||
if (script.metadata.storagename) {
|
||||
return script.metadata.storagename[0];
|
||||
}
|
||||
return script.uuid;
|
||||
}
|
||||
|
||||
async getScriptValue(script: Script) {
|
||||
const ret = await this.valueDAO.get(this.storageKey(script));
|
||||
const ret = await this.valueDAO.get(storageKey(script));
|
||||
if (!ret) {
|
||||
return {};
|
||||
}
|
||||
return Promise.resolve(ret?.data);
|
||||
}
|
||||
|
||||
async setValue(uuid: string, key: string, value: any): Promise<boolean> {
|
||||
// 查询出脚本
|
||||
const script = await this.scriptDAO.get(uuid);
|
||||
if (!script) {
|
||||
return Promise.reject(new Error("script not found"));
|
||||
}
|
||||
// 查询老的值
|
||||
const oldValue = await this.valueDAO.get(storageKey(script));
|
||||
if (!oldValue) {
|
||||
this.valueDAO.save(storageKey(script), {
|
||||
uuid: script.uuid,
|
||||
storageName: storageKey(script),
|
||||
data: { [key]: value },
|
||||
createtime: Date.now(),
|
||||
updatetime: Date.now(),
|
||||
});
|
||||
} else {
|
||||
oldValue.data[key] = value;
|
||||
this.valueDAO.save(storageKey(script), oldValue);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.group.on("getScriptValue", this.getScriptValue.bind(this));
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { WindowMessage } from "@Packages/message/window_message";
|
||||
|
||||
export function sendMessage(msg: WindowMessage, action: string, data?: any) {
|
||||
return msg.sendMessage({
|
||||
action,
|
||||
data,
|
||||
});
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import MessageInternal from "@App/app/message/internal";
|
||||
import { MessageSender } from "@App/app/message/message";
|
||||
import { ScriptMenu } from "@App/runtime/background/runtime";
|
||||
import { ScriptMenu } from "@App/runtime/service_worker/runtime";
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
|
25
src/pkg/utils/queue.ts
Normal file
25
src/pkg/utils/queue.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// 一个简单的队列,可以使用pop阻塞等待消息
|
||||
export default class Queue<T> {
|
||||
list: T[] = [];
|
||||
|
||||
resolve?: (data: T) => void;
|
||||
|
||||
push(data: T) {
|
||||
if (this.resolve) {
|
||||
this.resolve(data);
|
||||
this.resolve = undefined;
|
||||
} else {
|
||||
this.list.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
pop(): Promise<T | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
if (this.list.length > 0) {
|
||||
resolve(this.list.shift());
|
||||
} else {
|
||||
this.resolve = resolve;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -277,7 +277,7 @@ export function prepareScriptByCode(
|
||||
let old: Script | undefined;
|
||||
let oldCode: string | undefined;
|
||||
if (uuid) {
|
||||
old = await dao.findByUUID(uuid);
|
||||
old = await dao.get(uuid);
|
||||
if (!old && override) {
|
||||
old = await dao.findByNameAndNamespace(script.name, script.namespace);
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||
import { Value } from "@App/app/repo/value";
|
||||
import GMApi from "./gm_api";
|
||||
import { compileScript, createContext, proxyContext, ScriptFunc } from "./utils";
|
||||
import { Message } from "@Packages/message/server";
|
||||
|
||||
export type ValueUpdateData = {
|
||||
oldValue: any;
|
||||
value: Value;
|
||||
value: any;
|
||||
key: string; // 值key
|
||||
uuid: string;
|
||||
storageKey: string; // 储存key
|
||||
sender: {
|
||||
runFlag: string;
|
||||
tabId?: number;
|
||||
@ -30,7 +33,7 @@ export default class ExecScript {
|
||||
|
||||
GM_info: any;
|
||||
|
||||
constructor(scriptRes: ScriptRunResouce, thisContext?: { [key: string]: any }) {
|
||||
constructor(scriptRes: ScriptRunResouce, message: Message, thisContext?: { [key: string]: any }) {
|
||||
this.scriptRes = scriptRes;
|
||||
this.logger = LoggerCore.getInstance().logger({
|
||||
component: "exec",
|
||||
@ -49,7 +52,7 @@ export default class ExecScript {
|
||||
this.proxyContent = global;
|
||||
} else {
|
||||
// 构建脚本GM上下文
|
||||
this.sandboxContent = createContext(scriptRes, this.GM_info);
|
||||
this.sandboxContent = createContext(scriptRes, this.GM_info, message);
|
||||
this.proxyContent = proxyContext(global, this.sandboxContent, thisContext);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||
import ExecScript from "./exec_script";
|
||||
import { Message } from "@Packages/message/server";
|
||||
|
||||
export class CATRetryError {
|
||||
msg: string;
|
||||
@ -21,7 +22,7 @@ export class BgExecScriptWarp extends ExecScript {
|
||||
|
||||
setInterval: Map<number, boolean>;
|
||||
|
||||
constructor(scriptRes: ScriptRunResouce) {
|
||||
constructor(scriptRes: ScriptRunResouce, message: Message) {
|
||||
const thisContext: { [key: string]: any } = {};
|
||||
const setTimeout = new Map<number, any>();
|
||||
const setInterval = new Map<number, any>();
|
||||
@ -62,7 +63,7 @@ export class BgExecScriptWarp extends ExecScript {
|
||||
};
|
||||
// @ts-ignore
|
||||
thisContext.CATRetryError = CATRetryError;
|
||||
super(scriptRes, thisContext);
|
||||
super(scriptRes, message, thisContext);
|
||||
this.setTimeout = setTimeout;
|
||||
this.setInterval = setInterval;
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||
import { getMetadataStr, getUserConfigStr, parseUserConfig } from "@App/pkg/utils/script";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { ValueUpdateData } from "./exec_script";
|
||||
import { ExtVersion } from "@App/app/const";
|
||||
import { storageKey } from "../utils";
|
||||
import { Message } from "@Packages/message/server";
|
||||
|
||||
interface ApiParam {
|
||||
depend?: string[];
|
||||
@ -57,34 +58,44 @@ export default class GMApi {
|
||||
|
||||
valueChangeListener = new Map<number, { name: string; listener: GMTypes.ValueChangeListener }>();
|
||||
|
||||
constructor(private message: Message) {}
|
||||
|
||||
// 单次回调使用
|
||||
public sendMessage(api: string, params: any[]) {
|
||||
return null;
|
||||
return this.message.sendMessage({
|
||||
action: "serviceWorker/runtime/gmApi",
|
||||
data: {
|
||||
api,
|
||||
params,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 长连接使用,connect只用于接受消息,不能发送消息
|
||||
// 长连接使用,connect只用于接受消息,不发送消息
|
||||
public connect(api: string, params: any[]) {
|
||||
return null;
|
||||
return this.message.connect({
|
||||
action: "serviceWorker/runtime/gmApi",
|
||||
data: {
|
||||
api,
|
||||
params,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public valueUpdate(data: ValueUpdateData) {
|
||||
const { storagename } = this.scriptRes.metadata;
|
||||
if (
|
||||
data.value.uuid === this.scriptRes.uuid ||
|
||||
(storagename && data.value.storageName && storagename[0] === data.value.storageName)
|
||||
) {
|
||||
if (data.uuid === this.scriptRes.uuid || data.storageKey === storageKey(this.scriptRes)) {
|
||||
// 触发,并更新值
|
||||
if (data.value.value === undefined) {
|
||||
delete this.scriptRes.value[data.value.key];
|
||||
if (data.value === undefined) {
|
||||
delete this.scriptRes.value[data.value];
|
||||
} else {
|
||||
this.scriptRes.value[data.value.key] = data.value;
|
||||
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.value,
|
||||
data.value,
|
||||
data.sender.runFlag !== this.runFlag,
|
||||
data.sender.tabId
|
||||
);
|
||||
@ -130,4 +141,41 @@ export default class GMApi {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 获取脚本的值,可以通过@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]);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ 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";
|
||||
|
||||
// 构建脚本运行代码
|
||||
export function compileScriptCode(scriptRes: ScriptRunResouce, code: string): string {
|
||||
@ -52,9 +53,10 @@ function setDepend(context: { [key: string]: any }, apiVal: ApiValue) {
|
||||
}
|
||||
|
||||
// 构建沙盒上下文
|
||||
export function createContext(scriptRes: ScriptRunResouce, GMInfo: any): GMApi {
|
||||
export function createContext(scriptRes: ScriptRunResouce, GMInfo: any, message: Message): GMApi {
|
||||
// 按照GMApi构建
|
||||
const context: { [key: string]: any } = {
|
||||
message: message,
|
||||
scriptRes,
|
||||
valueChangeListener: new Map<number, { name: string; listener: GMTypes.ValueChangeListener }>(),
|
||||
sendMessage: GMApi.prototype.sendMessage,
|
||||
|
47
src/runtime/service_worker/gm_api.ts
Normal file
47
src/runtime/service_worker/gm_api.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import { Script } from "@App/app/repo/scripts";
|
||||
import PermissionVerify from "./permission_verify";
|
||||
import { MessageSender } from "@Packages/message/server";
|
||||
import { ValueService } from "@App/app/service/service_worker/value";
|
||||
|
||||
// GMApi,处理脚本的GM API调用请求
|
||||
|
||||
export type MessageRequest = {
|
||||
scriptId: number; // 脚本id
|
||||
api: string;
|
||||
runFlag: string;
|
||||
params: any[];
|
||||
};
|
||||
|
||||
export type Request = MessageRequest & {
|
||||
script: Script;
|
||||
sender: MessageSender;
|
||||
};
|
||||
|
||||
export type Api = (request: Request) => Promise<any>;
|
||||
|
||||
export default class GMApi {
|
||||
logger: Logger;
|
||||
|
||||
constructor(private value: ValueService) {
|
||||
this.logger = LoggerCore.logger().with({ service: "runtime/gm_api" });
|
||||
}
|
||||
|
||||
handlerRequest(params: Request) {
|
||||
console.log(params);
|
||||
}
|
||||
|
||||
@PermissionVerify.API()
|
||||
GM_setValue(request: Request): Promise<any> {
|
||||
if (!request.params || request.params.length !== 2) {
|
||||
return Promise.reject(new Error("param is failed"));
|
||||
}
|
||||
const [key, value] = request.params;
|
||||
const sender = <MessageSender & { runFlag: string }>request.sender;
|
||||
sender.runFlag = request.runFlag;
|
||||
return this.value.setValue(request.script.uuid, key, value);
|
||||
}
|
||||
|
||||
start() {}
|
||||
}
|
383
src/runtime/service_worker/permission_verify.ts
Normal file
383
src/runtime/service_worker/permission_verify.ts
Normal file
@ -0,0 +1,383 @@
|
||||
// gm api 权限验证
|
||||
import Cache from "@App/app/cache";
|
||||
import { Permission, PermissionDAO } from "@App/app/repo/permission";
|
||||
import { Script } from "@App/app/repo/scripts";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Api, Request } from "./gm_api";
|
||||
import Queue from "@App/pkg/utils/queue";
|
||||
|
||||
export interface ConfirmParam {
|
||||
// 权限名
|
||||
permission: string;
|
||||
// 权限值
|
||||
permissionValue?: string;
|
||||
// 确认权限标题
|
||||
title?: string;
|
||||
// 权限详情内容
|
||||
metadata?: { [key: string]: string };
|
||||
// 权限描述
|
||||
describe?: string;
|
||||
// 是否通配
|
||||
wildcard?: boolean;
|
||||
// 权限内容
|
||||
permissionContent?: string;
|
||||
}
|
||||
|
||||
export interface UserConfirm {
|
||||
allow: boolean;
|
||||
type: number; // 1: 允许一次 2: 临时允许全部 3: 临时允许此 4: 永久允许全部 5: 永久允许此
|
||||
}
|
||||
|
||||
export interface ApiParam {
|
||||
// 默认提供的函数
|
||||
default?: boolean;
|
||||
// 是否只有后台环境中才能执行
|
||||
background?: boolean;
|
||||
// 是否需要弹出页面让用户进行确认
|
||||
confirm?: (request: Request) => Promise<boolean | ConfirmParam>;
|
||||
// 监听方法
|
||||
listener?: () => void;
|
||||
// 别名
|
||||
alias?: string[];
|
||||
// 关联
|
||||
link?: string;
|
||||
}
|
||||
|
||||
export interface ApiValue {
|
||||
api: Api;
|
||||
param: ApiParam;
|
||||
}
|
||||
|
||||
export interface IPermissionVerify {
|
||||
verify(request: Request, api: ApiValue): Promise<boolean>;
|
||||
}
|
||||
|
||||
export default class PermissionVerify {
|
||||
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();
|
||||
}
|
||||
PermissionVerify.apis.set(key, {
|
||||
api: descriptor.value,
|
||||
param,
|
||||
});
|
||||
// 兼容GM.*
|
||||
const dot = key.replace("_", ".");
|
||||
if (dot !== key) {
|
||||
PermissionVerify.apis.set(dot, {
|
||||
api: descriptor.value,
|
||||
param,
|
||||
});
|
||||
if (param.alias) {
|
||||
param.alias.push(dot);
|
||||
} else {
|
||||
param.alias = [dot];
|
||||
}
|
||||
}
|
||||
|
||||
// 处理别名
|
||||
if (param.alias) {
|
||||
param.alias.forEach((alias) => {
|
||||
PermissionVerify.apis.set(alias, {
|
||||
api: descriptor.value,
|
||||
param,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
permissionDAO: PermissionDAO;
|
||||
|
||||
// 确认队列
|
||||
confirmQueue: Queue<{
|
||||
request: Request;
|
||||
confirm: ConfirmParam | boolean;
|
||||
resolve: (value: boolean) => void;
|
||||
reject: (reason: any) => void;
|
||||
}> = new Queue();
|
||||
|
||||
removePermissionCache(scriptId: number) {
|
||||
// 先删除缓存
|
||||
Cache.getInstance()
|
||||
.list()
|
||||
.forEach((key) => {
|
||||
if (key.startsWith(`permission:${scriptId.toString()}:`)) {
|
||||
Cache.getInstance().del(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.permissionDAO = new PermissionDAO();
|
||||
// 监听用户确认消息
|
||||
const message = <MessageHander>IoC.instance(MessageHander);
|
||||
message.setHandler("permissionConfirm", (_action, data: { uuid: string; userConfirm: UserConfirm }) => {
|
||||
const confirm = this.confirmMap.get(data.uuid);
|
||||
if (!confirm) {
|
||||
if (data.userConfirm.type === 0) {
|
||||
// 忽略
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
return Promise.reject(new Error("confirm not found"));
|
||||
}
|
||||
this.confirmMap.delete(data.uuid);
|
||||
confirm.resolve(data.userConfirm);
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
// 监听获取用户确认消息
|
||||
message.setHandler("getConfirm", (_action, uuid: string) => {
|
||||
const data = this.confirmMap.get(uuid);
|
||||
if (!data) {
|
||||
return Promise.reject(new Error("uuid not found"));
|
||||
}
|
||||
// 查询允许统配的有多少个相同等待确认权限
|
||||
let likeNum = 0;
|
||||
if (data.confirm.wildcard) {
|
||||
this.confirmQueue.list.forEach((value) => {
|
||||
const confirm = value.confirm as ConfirmParam;
|
||||
if (
|
||||
confirm.wildcard &&
|
||||
value.request.scriptId === data.script.id &&
|
||||
confirm.permission === data.confirm.permission
|
||||
) {
|
||||
likeNum += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
script: data.script,
|
||||
confirm: data.confirm,
|
||||
likeNum,
|
||||
});
|
||||
});
|
||||
// 监听删除权限
|
||||
message.setHandler("deletePermission", async (_action, data: { scriptId: number; confirm: ConfirmParam }) => {
|
||||
// 先删除缓存
|
||||
this.removePermissionCache(data.scriptId);
|
||||
// 再删除数据库
|
||||
const m = await this.permissionDAO.findOne({
|
||||
scriptId: data.scriptId,
|
||||
permission: data.confirm.permission,
|
||||
permissionValue: data.confirm.permissionValue || "",
|
||||
});
|
||||
if (!m) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
await this.permissionDAO.delete(m.id);
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
// 监听添加权限
|
||||
message.setHandler("addPermission", async (_action, data: { scriptId: number; permission: Permission }) => {
|
||||
// 先删除缓存
|
||||
this.removePermissionCache(data.scriptId);
|
||||
// 从数据库中查询是否有此权限
|
||||
const m = await this.permissionDAO.findOne({
|
||||
scriptId: data.scriptId,
|
||||
permission: data.permission.permission,
|
||||
permissionValue: data.permission.permissionValue || "",
|
||||
});
|
||||
if (!m) {
|
||||
// 没有添加
|
||||
await this.permissionDAO.save(data.permission);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
// 有则更新
|
||||
data.permission.id = m.id;
|
||||
data.permission.createtime = m.createtime;
|
||||
data.permission.updatetime = new Date().getTime();
|
||||
this.permissionDAO.update(m.id, data.permission);
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
// 监听重置权限
|
||||
message.setHandler("resetPermission", async (_action, data: { scriptId: number }) => {
|
||||
// 先删除缓存
|
||||
this.removePermissionCache(data.scriptId);
|
||||
// 从数据库中查询是否有此权限
|
||||
await this.permissionDAO.delete({
|
||||
scriptId: data.scriptId,
|
||||
});
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
this.dealConfirmQueue();
|
||||
}
|
||||
|
||||
// 验证是否有权限
|
||||
verify(request: Request, api: ApiValue): Promise<boolean> {
|
||||
if (api.param.default) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
// 没有其它条件,从metadata.grant中判断
|
||||
const { grant } = request.script.metadata;
|
||||
if (!grant) {
|
||||
return Promise.reject(new Error("grant is undefined"));
|
||||
}
|
||||
for (let i = 0; i < grant.length; i += 1) {
|
||||
if (
|
||||
// 名称相等
|
||||
grant[i] === request.api ||
|
||||
// 别名相等
|
||||
(api.param.alias && api.param.alias.includes(grant[i])) ||
|
||||
// 有关联的
|
||||
grant[i] === api.param.link
|
||||
) {
|
||||
// 需要用户确认
|
||||
if (api.param.confirm) {
|
||||
return this.pushConfirmQueue(request, api);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
return Promise.reject(new Error("permission not requested"));
|
||||
}
|
||||
|
||||
async dealConfirmQueue() {
|
||||
// 处理确认队列
|
||||
const data = await this.confirmQueue.pop();
|
||||
if (!data) {
|
||||
this.dealConfirmQueue();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const ret = await this.confirm(data.request, data.confirm);
|
||||
data.resolve(ret);
|
||||
} catch (e) {
|
||||
data.reject(e);
|
||||
}
|
||||
this.dealConfirmQueue();
|
||||
}
|
||||
|
||||
// 确认队列,为了防止一次性打开过多的窗口
|
||||
async pushConfirmQueue(request: Request, api: ApiValue): Promise<boolean> {
|
||||
const confirm = await api.param.confirm!(request);
|
||||
if (confirm === true) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.confirmQueue.push({ request, confirm, resolve, reject });
|
||||
});
|
||||
}
|
||||
|
||||
async confirm(request: Request, confirm: boolean | ConfirmParam): Promise<boolean> {
|
||||
if (typeof confirm === "boolean") {
|
||||
return confirm;
|
||||
}
|
||||
const cacheKey = CacheKey.permissionConfirm(request.script.id, confirm);
|
||||
// 从数据库中查询是否有此权限
|
||||
const ret = await Cache.getInstance().getOrSet(cacheKey, async () => {
|
||||
let model = await this.permissionDAO.findOne({
|
||||
scriptId: request.scriptId,
|
||||
permission: confirm.permission,
|
||||
permissionValue: confirm.permissionValue || "",
|
||||
});
|
||||
if (!model) {
|
||||
// 允许通配
|
||||
if (confirm.wildcard) {
|
||||
model = await this.permissionDAO.findOne({
|
||||
scriptId: request.scriptId,
|
||||
permission: confirm.permission,
|
||||
permissionValue: "*",
|
||||
});
|
||||
}
|
||||
}
|
||||
return Promise.resolve(model);
|
||||
});
|
||||
// 有查询到结果,进入判断,不再需要用户确认
|
||||
if (ret) {
|
||||
if (ret.allow) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
// 权限拒绝
|
||||
return Promise.reject(new Error("permission denied"));
|
||||
}
|
||||
// 没有权限,则弹出页面让用户进行确认
|
||||
const userConfirm = await this.confirmWindow(request.script, confirm);
|
||||
// 成功存入数据库
|
||||
const model = {
|
||||
id: 0,
|
||||
scriptId: request.scriptId,
|
||||
permission: confirm.permission,
|
||||
permissionValue: "",
|
||||
allow: userConfirm.allow,
|
||||
createtime: new Date().getTime(),
|
||||
updatetime: 0,
|
||||
};
|
||||
switch (userConfirm.type) {
|
||||
case 4:
|
||||
case 2: {
|
||||
// 通配
|
||||
model.permissionValue = "*";
|
||||
break;
|
||||
}
|
||||
case 5:
|
||||
case 3: {
|
||||
model.permissionValue = confirm.permissionValue || "";
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// 临时 放入缓存
|
||||
if (userConfirm.type >= 2) {
|
||||
Cache.getInstance().set(cacheKey, model);
|
||||
}
|
||||
// 总是 放入数据库
|
||||
if (userConfirm.type >= 4) {
|
||||
const oldConfirm = await this.permissionDAO.findOne({
|
||||
scriptId: request.scriptId,
|
||||
permission: model.permission,
|
||||
permissionValue: model.permissionValue,
|
||||
});
|
||||
if (!oldConfirm) {
|
||||
await this.permissionDAO.save(model);
|
||||
} else {
|
||||
await this.permissionDAO.update(oldConfirm.id, model);
|
||||
}
|
||||
}
|
||||
if (userConfirm.allow) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.reject(new Error("permission not allowed"));
|
||||
}
|
||||
|
||||
// 确认map
|
||||
confirmMap: Map<
|
||||
string,
|
||||
{
|
||||
confirm: ConfirmParam;
|
||||
script: Script;
|
||||
resolve: (value: UserConfirm) => void;
|
||||
reject: (reason: any) => void;
|
||||
}
|
||||
> = new Map();
|
||||
|
||||
// 弹出窗口让用户进行确认
|
||||
async confirmWindow(script: Script, confirm: ConfirmParam): Promise<UserConfirm> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const uuid = uuidv4();
|
||||
// 超时处理
|
||||
const timeout = setTimeout(() => {
|
||||
this.confirmMap.delete(uuid);
|
||||
reject(new Error("permission confirm timeout"));
|
||||
}, 40 * 1000);
|
||||
// 保存到map中
|
||||
this.confirmMap.set(uuid, {
|
||||
confirm,
|
||||
script,
|
||||
resolve: (value: UserConfirm) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(value);
|
||||
},
|
||||
reject,
|
||||
});
|
||||
// 打开窗口
|
||||
chrome.tabs.create({
|
||||
url: chrome.runtime.getURL(`src/confirm.html?uuid=${uuid}`),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
735
src/runtime/service_worker/runtime.ts
Normal file
735
src/runtime/service_worker/runtime.ts
Normal file
@ -0,0 +1,735 @@
|
||||
// 脚本运行时,主要负责脚本的加载和匹配
|
||||
// 油猴脚本将监听页面的创建,将代码注入到页面中
|
||||
import MessageSandbox from "@App/app/message/sandbox";
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import {
|
||||
Script,
|
||||
SCRIPT_RUN_STATUS,
|
||||
SCRIPT_STATUS_ENABLE,
|
||||
SCRIPT_TYPE_NORMAL,
|
||||
ScriptDAO,
|
||||
ScriptRunResouce,
|
||||
SCRIPT_RUN_STATUS_RUNNING,
|
||||
Metadata,
|
||||
} from "@App/app/repo/scripts";
|
||||
import ResourceManager from "@App/app/service/resource/manager";
|
||||
import ValueManager from "@App/app/service/value/manager";
|
||||
import { dealScript, randomString } from "@App/pkg/utils/utils";
|
||||
import { UrlInclude, UrlMatch } from "@App/pkg/utils/match";
|
||||
import {
|
||||
MessageHander,
|
||||
MessageSender,
|
||||
TargetTag,
|
||||
} from "@App/app/message/message";
|
||||
import ScriptManager from "@App/app/service/script/manager";
|
||||
import { Channel } from "@App/app/message/channel";
|
||||
import IoC from "@App/app/ioc";
|
||||
import Manager from "@App/app/service/manager";
|
||||
import Hook from "@App/app/service/hook";
|
||||
import { i18nName } from "@App/locales/locales";
|
||||
import { compileInjectScript, compileScriptCode } from "../content/utils";
|
||||
import GMApi, { Request } from "./gm_api";
|
||||
import { genScriptMenu } from "./utils";
|
||||
|
||||
export type RuntimeEvent = "start" | "stop" | "watchRunStatus";
|
||||
|
||||
export type ScriptMenuItem = {
|
||||
id: number;
|
||||
name: string;
|
||||
accessKey?: string;
|
||||
sender: MessageSender;
|
||||
channelFlag: string;
|
||||
};
|
||||
|
||||
export type ScriptMenu = {
|
||||
id: number;
|
||||
name: string;
|
||||
enable: boolean;
|
||||
updatetime: number;
|
||||
hasUserConfig: boolean;
|
||||
metadata: Metadata;
|
||||
runStatus?: SCRIPT_RUN_STATUS;
|
||||
runNum: number;
|
||||
runNumByIframe: number;
|
||||
menus?: ScriptMenuItem[];
|
||||
customExclude?: string[];
|
||||
};
|
||||
|
||||
// 后台脚本将会将代码注入到沙盒中
|
||||
@IoC.Singleton(MessageHander, ResourceManager, ValueManager)
|
||||
export default class Runtime extends Manager {
|
||||
messageSandbox?: MessageSandbox;
|
||||
|
||||
scriptDAO: ScriptDAO;
|
||||
|
||||
resourceManager: ResourceManager;
|
||||
|
||||
valueManager: ValueManager;
|
||||
|
||||
logger: Logger;
|
||||
|
||||
match: UrlMatch<ScriptRunResouce> = new UrlMatch();
|
||||
|
||||
include: UrlInclude<ScriptRunResouce> = new UrlInclude();
|
||||
|
||||
// 自定义排除
|
||||
customizeExclude: UrlMatch<ScriptRunResouce> = new UrlMatch();
|
||||
|
||||
static hook = new Hook<"runStatus">();
|
||||
|
||||
// 运行中和开启的后台脚本
|
||||
runBackScript: Map<number, Script> = new Map();
|
||||
|
||||
constructor(
|
||||
message: MessageHander,
|
||||
resourceManager: ResourceManager,
|
||||
valueManager: ValueManager
|
||||
) {
|
||||
super(message, "runtime");
|
||||
this.scriptDAO = new ScriptDAO();
|
||||
this.resourceManager = resourceManager;
|
||||
this.valueManager = valueManager;
|
||||
this.logger = LoggerCore.getInstance().logger({ component: "runtime" });
|
||||
ScriptManager.hook.addListener("upsert", this.scriptUpdate.bind(this));
|
||||
ScriptManager.hook.addListener("delete", this.scriptDelete.bind(this));
|
||||
ScriptManager.hook.addListener("enable", this.scriptUpdate.bind(this));
|
||||
ScriptManager.hook.addListener("disable", this.scriptUpdate.bind(this));
|
||||
}
|
||||
|
||||
start(): void {
|
||||
// 监听前端消息
|
||||
// 此处是处理执行单次脚本的消息
|
||||
this.listenEvent("start", (id) => {
|
||||
return this.scriptDAO
|
||||
.findById(id)
|
||||
.then((script) => {
|
||||
if (!script) {
|
||||
throw new Error("script not found");
|
||||
}
|
||||
// 因为如果直接引用Runtime,会导致循环依赖,暂时这样处理,后面再梳理梳理
|
||||
return this.startBackgroundScript(script);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger.error("run error", Logger.E(e));
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
|
||||
this.listenEvent("stop", (id) => {
|
||||
return this.scriptDAO
|
||||
.findById(id)
|
||||
.then((script) => {
|
||||
if (!script) {
|
||||
throw new Error("script not found");
|
||||
}
|
||||
// 因为如果直接引用Runtime,会导致循环依赖,暂时这样处理
|
||||
return this.stopBackgroundScript(id);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger.error("stop error", Logger.E(e));
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
// 监听脚本运行状态
|
||||
this.listenScriptRunStatus();
|
||||
|
||||
// 启动普通脚本
|
||||
this.scriptDAO.table.toArray((items) => {
|
||||
items.forEach((item) => {
|
||||
// 容错处理
|
||||
if (!item) {
|
||||
this.logger.error("script is null");
|
||||
return;
|
||||
}
|
||||
if (item.type !== SCRIPT_TYPE_NORMAL) {
|
||||
return;
|
||||
}
|
||||
// 加载所有的脚本
|
||||
if (item.status === SCRIPT_STATUS_ENABLE) {
|
||||
this.enable(item);
|
||||
} else {
|
||||
// 只处理未开启的普通页面脚本
|
||||
this.disable(item);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 接受消息,注入脚本
|
||||
// 获取注入源码
|
||||
|
||||
// 监听菜单创建
|
||||
const scriptMenu: Map<
|
||||
number | TargetTag,
|
||||
Map<
|
||||
number,
|
||||
{
|
||||
request: Request;
|
||||
channel: Channel;
|
||||
}[]
|
||||
>
|
||||
> = new Map();
|
||||
GMApi.hook.addListener(
|
||||
"registerMenu",
|
||||
(request: Request, channel: Channel) => {
|
||||
let senderId: number | TargetTag;
|
||||
if (!request.sender.tabId) {
|
||||
// 非页面脚本
|
||||
senderId = request.sender.targetTag;
|
||||
} else {
|
||||
senderId = request.sender.tabId;
|
||||
}
|
||||
let tabMap = scriptMenu.get(senderId);
|
||||
if (!tabMap) {
|
||||
tabMap = new Map();
|
||||
scriptMenu.set(senderId, tabMap);
|
||||
}
|
||||
let menuArr = tabMap.get(request.scriptId);
|
||||
if (!menuArr) {
|
||||
menuArr = [];
|
||||
tabMap.set(request.scriptId, menuArr);
|
||||
}
|
||||
// 查询菜单是否已经存在
|
||||
for (let i = 0; i < menuArr.length; i += 1) {
|
||||
// id 相等 跳过,选第一个,并close链接
|
||||
if (menuArr[i].request.params[0] === request.params[0]) {
|
||||
channel.disChannel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
menuArr.push({ request, channel });
|
||||
// 偷懒行为, 直接重新生成菜单
|
||||
genScriptMenu(senderId, scriptMenu);
|
||||
}
|
||||
);
|
||||
GMApi.hook.addListener("unregisterMenu", (id, request: Request) => {
|
||||
let senderId: number | TargetTag;
|
||||
if (!request.sender.tabId) {
|
||||
// 非页面脚本
|
||||
senderId = request.sender.targetTag;
|
||||
} else {
|
||||
senderId = request.sender.tabId;
|
||||
}
|
||||
const tabMap = scriptMenu.get(senderId);
|
||||
if (tabMap) {
|
||||
const menuArr = tabMap.get(request.scriptId);
|
||||
if (menuArr) {
|
||||
// 从菜单数组中遍历删除
|
||||
for (let i = 0; i < menuArr.length; i += 1) {
|
||||
if (menuArr[i].request.params[0] === id) {
|
||||
menuArr.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (menuArr.length === 0) {
|
||||
tabMap.delete(request.scriptId);
|
||||
}
|
||||
}
|
||||
if (!tabMap.size) {
|
||||
scriptMenu.delete(senderId);
|
||||
}
|
||||
}
|
||||
// 偷懒行为
|
||||
genScriptMenu(senderId, scriptMenu);
|
||||
});
|
||||
|
||||
// 监听页面切换加载菜单
|
||||
chrome.tabs.onActivated.addListener((activeInfo) => {
|
||||
genScriptMenu(activeInfo.tabId, scriptMenu);
|
||||
});
|
||||
|
||||
Runtime.hook.addListener("runStatus", async (scriptId: number) => {
|
||||
const script = await this.scriptDAO.findById(scriptId);
|
||||
if (!script) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
script.status !== SCRIPT_STATUS_ENABLE &&
|
||||
script.runStatus !== "running"
|
||||
) {
|
||||
// 没开启并且不是运行中的脚本,删除
|
||||
this.runBackScript.delete(scriptId);
|
||||
} else {
|
||||
// 否则进行一次更新
|
||||
this.runBackScript.set(scriptId, script);
|
||||
}
|
||||
});
|
||||
|
||||
// 记录运行次数与iframe运行
|
||||
const runScript = new Map<
|
||||
number,
|
||||
Map<number, { script: Script; runNum: number; runNumByIframe: number }>
|
||||
>();
|
||||
const addRunScript = (
|
||||
tabId: number,
|
||||
script: Script,
|
||||
iframe: boolean,
|
||||
num: number = 1
|
||||
) => {
|
||||
let scripts = runScript.get(tabId);
|
||||
if (!scripts) {
|
||||
scripts = new Map();
|
||||
runScript.set(tabId, scripts);
|
||||
}
|
||||
let scriptNum = scripts.get(script.id);
|
||||
if (!scriptNum) {
|
||||
scriptNum = { script, runNum: 0, runNumByIframe: 0 };
|
||||
scripts.set(script.id, scriptNum);
|
||||
}
|
||||
if (script.status === SCRIPT_STATUS_ENABLE) {
|
||||
scriptNum.runNum += num;
|
||||
if (iframe) {
|
||||
scriptNum.runNumByIframe += num;
|
||||
}
|
||||
}
|
||||
};
|
||||
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||
runScript.delete(tabId);
|
||||
});
|
||||
// 给popup页面获取运行脚本,与菜单
|
||||
this.message.setHandler(
|
||||
"queryPageScript",
|
||||
async (action: string, { url, tabId }: any) => {
|
||||
const tabMap = scriptMenu.get(tabId);
|
||||
const run = runScript.get(tabId);
|
||||
let matchScripts = [];
|
||||
if (!run) {
|
||||
matchScripts = this.matchUrl(url).map((item) => {
|
||||
return { runNum: 0, runNumByIframe: 0, script: item };
|
||||
});
|
||||
} else {
|
||||
matchScripts = Array.from(run.values());
|
||||
}
|
||||
const allPromise: Promise<ScriptMenu>[] = matchScripts.map(
|
||||
async (item) => {
|
||||
const menus: ScriptMenuItem[] = [];
|
||||
if (tabMap) {
|
||||
tabMap.get(item.script.id)?.forEach((scriptItem) => {
|
||||
menus.push({
|
||||
name: scriptItem.request.params[1],
|
||||
accessKey: scriptItem.request.params[2],
|
||||
id: scriptItem.request.params[0],
|
||||
sender: scriptItem.request.sender,
|
||||
channelFlag: scriptItem.channel.flag,
|
||||
});
|
||||
});
|
||||
}
|
||||
const script = await this.scriptDAO.findById(item.script.id);
|
||||
if (!script) {
|
||||
return {
|
||||
id: item.script.id,
|
||||
name: i18nName(item.script),
|
||||
enable: item.script.status === SCRIPT_STATUS_ENABLE,
|
||||
updatetime: item.script.updatetime || item.script.createtime,
|
||||
metadata: item.script.metadata,
|
||||
hasUserConfig: !!item.script.config,
|
||||
runNum: item.runNum,
|
||||
runNumByIframe: item.runNumByIframe,
|
||||
customExclude:
|
||||
item.script.selfMetadata && item.script.selfMetadata.exclude,
|
||||
menus,
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: script.id,
|
||||
name: i18nName(script),
|
||||
enable: script.status === SCRIPT_STATUS_ENABLE,
|
||||
updatetime: script.updatetime || script.createtime,
|
||||
metadata: item.script.metadata,
|
||||
hasUserConfig: !!script?.config,
|
||||
runNum: item.runNum,
|
||||
runNumByIframe: item.runNumByIframe,
|
||||
customExclude: script.selfMetadata && script.selfMetadata.exclude,
|
||||
menus,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const scriptList: ScriptMenu[] = await Promise.all(allPromise);
|
||||
|
||||
const backScriptList: ScriptMenu[] = [];
|
||||
const sandboxMenuMap = scriptMenu.get("sandbox");
|
||||
this.runBackScript.forEach((item) => {
|
||||
const menus: ScriptMenuItem[] = [];
|
||||
if (sandboxMenuMap) {
|
||||
sandboxMenuMap?.get(item.id)?.forEach((scriptItem) => {
|
||||
menus.push({
|
||||
name: scriptItem.request.params[1],
|
||||
accessKey: scriptItem.request.params[2],
|
||||
id: scriptItem.request.params[0],
|
||||
sender: scriptItem.request.sender,
|
||||
channelFlag: scriptItem.channel.flag,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
backScriptList.push({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
enable: item.status === SCRIPT_STATUS_ENABLE,
|
||||
updatetime: item.updatetime || item.createtime,
|
||||
metadata: item.metadata,
|
||||
runStatus: item.runStatus,
|
||||
hasUserConfig: !!item.config,
|
||||
runNum:
|
||||
item.runStatus && item.runStatus === SCRIPT_RUN_STATUS_RUNNING
|
||||
? 1
|
||||
: 0,
|
||||
menus,
|
||||
runNumByIframe: 0,
|
||||
});
|
||||
});
|
||||
return Promise.resolve({
|
||||
scriptList,
|
||||
backScriptList,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// content页发送页面加载完成消息,注入脚本
|
||||
this.message.setHandler(
|
||||
"pageLoad",
|
||||
(_action: string, data: any, sender: MessageSender) => {
|
||||
return new Promise((resolve) => {
|
||||
if (!sender) {
|
||||
return;
|
||||
}
|
||||
if (!(sender.url && sender.tabId)) {
|
||||
return;
|
||||
}
|
||||
if (sender.frameId === undefined) {
|
||||
// 清理之前的数据
|
||||
runScript.delete(sender.tabId);
|
||||
}
|
||||
// 未开启
|
||||
if (localStorage.enable_script === "false") {
|
||||
return;
|
||||
}
|
||||
const exclude = this.customizeExclude.match(sender.url);
|
||||
// 自定义排除的, buildScriptRunResource时会将selfMetadata合并,所以后续不需要再处理metadata.exclude,这算是一个隐性的坑,后面看看要不要处理
|
||||
exclude.forEach((val) => {
|
||||
addRunScript(sender.tabId!, val, false, 0);
|
||||
});
|
||||
const filter: ScriptRunResouce[] = this.matchUrl(
|
||||
sender.url,
|
||||
(script) => {
|
||||
// 如果是iframe,判断是否允许在iframe里运行
|
||||
if (sender.frameId !== undefined) {
|
||||
if (script.metadata.noframes) {
|
||||
return true;
|
||||
}
|
||||
addRunScript(sender.tabId!, script, true);
|
||||
return script.status !== SCRIPT_STATUS_ENABLE;
|
||||
}
|
||||
addRunScript(sender.tabId!, script, false);
|
||||
return script.status !== SCRIPT_STATUS_ENABLE;
|
||||
}
|
||||
);
|
||||
|
||||
if (!filter.length) {
|
||||
resolve({ scripts: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({ scripts: filter });
|
||||
|
||||
// 注入脚本
|
||||
filter.forEach((script) => {
|
||||
let runAt = "document_idle";
|
||||
if (script.metadata["run-at"]) {
|
||||
[runAt] = script.metadata["run-at"];
|
||||
}
|
||||
switch (runAt) {
|
||||
case "document-body":
|
||||
case "document-start":
|
||||
runAt = "document_start";
|
||||
break;
|
||||
case "document-end":
|
||||
runAt = "document_end";
|
||||
break;
|
||||
case "document-idle":
|
||||
default:
|
||||
runAt = "document_idle";
|
||||
break;
|
||||
}
|
||||
chrome.tabs.executeScript(sender.tabId!, {
|
||||
frameId: sender.frameId,
|
||||
code: `(function(){
|
||||
let temp = document.createElementNS("http://www.w3.org/1999/xhtml", "script");
|
||||
temp.setAttribute('type', 'text/javascript');
|
||||
temp.innerHTML = "${script.code}";
|
||||
temp.className = "injected-js";
|
||||
document.documentElement.appendChild(temp);
|
||||
temp.remove();
|
||||
}())`,
|
||||
runAt,
|
||||
});
|
||||
});
|
||||
|
||||
// 角标和脚本
|
||||
chrome.browserAction.getBadgeText(
|
||||
{
|
||||
tabId: sender.tabId,
|
||||
},
|
||||
(res: string) => {
|
||||
chrome.browserAction.setBadgeText({
|
||||
text: (filter.length + (parseInt(res, 10) || 0)).toString(),
|
||||
tabId: sender.tabId,
|
||||
});
|
||||
}
|
||||
);
|
||||
chrome.browserAction.setBadgeBackgroundColor({
|
||||
color: "#4e5969",
|
||||
tabId: sender.tabId,
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setMessageSandbox(messageSandbox: MessageSandbox) {
|
||||
this.messageSandbox = messageSandbox;
|
||||
}
|
||||
|
||||
// 启动沙盒相关脚本
|
||||
startSandbox(messageSandbox: MessageSandbox) {
|
||||
this.messageSandbox = messageSandbox;
|
||||
this.scriptDAO.table.toArray((items) => {
|
||||
items.forEach((item) => {
|
||||
// 容错处理
|
||||
if (!item) {
|
||||
this.logger.error("script is null");
|
||||
return;
|
||||
}
|
||||
if (item.type === SCRIPT_TYPE_NORMAL) {
|
||||
return;
|
||||
}
|
||||
// 加载所有的脚本
|
||||
if (item.status === SCRIPT_STATUS_ENABLE) {
|
||||
this.enable(item);
|
||||
this.runBackScript.set(item.id, item);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
listenScriptRunStatus() {
|
||||
// 监听沙盒发送的脚本运行状态消息
|
||||
this.message.setHandler(
|
||||
"scriptRunStatus",
|
||||
(action, [scriptId, runStatus, error, nextruntime]: any) => {
|
||||
this.scriptDAO.update(scriptId, {
|
||||
runStatus,
|
||||
lastruntime: new Date().getTime(),
|
||||
nextruntime,
|
||||
error,
|
||||
});
|
||||
Runtime.hook.trigger("runStatus", scriptId, runStatus);
|
||||
}
|
||||
);
|
||||
// 处理前台发送的脚本运行状态监听请求
|
||||
this.message.setHandlerWithChannel("watchRunStatus", (channel) => {
|
||||
const hook = (scriptId: number, status: SCRIPT_RUN_STATUS) => {
|
||||
channel.send([scriptId, status]);
|
||||
};
|
||||
Runtime.hook.addListener("runStatus", hook);
|
||||
channel.setDisChannelHandler(() => {
|
||||
Runtime.hook.removeListener("runStatus", hook);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 脚本发生变动
|
||||
async scriptUpdate(script: Script): Promise<boolean> {
|
||||
// 脚本更新先更新资源
|
||||
await this.resourceManager.checkScriptResource(script);
|
||||
if (script.status === SCRIPT_STATUS_ENABLE) {
|
||||
return this.enable(script as ScriptRunResouce);
|
||||
}
|
||||
return this.disable(script);
|
||||
}
|
||||
|
||||
matchUrl(url: string, filterFunc?: (script: Script) => boolean) {
|
||||
const scripts = this.match.match(url);
|
||||
// 再include中匹配
|
||||
scripts.push(...this.include.match(url));
|
||||
const filter: { [key: string]: ScriptRunResouce } = {};
|
||||
// 去重
|
||||
scripts.forEach((script) => {
|
||||
if (filterFunc && filterFunc(script)) {
|
||||
return;
|
||||
}
|
||||
filter[script.id] = script;
|
||||
});
|
||||
// 转换成数组
|
||||
return Object.keys(filter).map((key) => filter[key]);
|
||||
}
|
||||
|
||||
// 脚本删除
|
||||
async scriptDelete(script: Script): Promise<boolean> {
|
||||
// 清理匹配资源
|
||||
if (script.type === SCRIPT_TYPE_NORMAL) {
|
||||
this.match.del(<ScriptRunResouce>script);
|
||||
this.include.del(<ScriptRunResouce>script);
|
||||
} else {
|
||||
this.unloadBackgroundScript(script);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// 脚本开启
|
||||
async enable(script: Script): Promise<boolean> {
|
||||
// 编译脚本运行资源
|
||||
const scriptRes = await this.buildScriptRunResource(script);
|
||||
if (script.type !== SCRIPT_TYPE_NORMAL) {
|
||||
return this.loadBackgroundScript(scriptRes);
|
||||
}
|
||||
return this.loadPageScript(scriptRes);
|
||||
}
|
||||
|
||||
// 脚本关闭
|
||||
disable(script: Script): Promise<boolean> {
|
||||
if (script.type !== SCRIPT_TYPE_NORMAL) {
|
||||
return this.unloadBackgroundScript(script);
|
||||
}
|
||||
return this.unloadPageScript(script);
|
||||
}
|
||||
|
||||
// 加载页面脚本
|
||||
loadPageScript(script: ScriptRunResouce) {
|
||||
// 重构code
|
||||
const logger = this.logger.with({
|
||||
scriptId: script.id,
|
||||
name: script.name,
|
||||
});
|
||||
script.code = dealScript(compileInjectScript(script));
|
||||
|
||||
this.match.del(<ScriptRunResouce>script);
|
||||
this.include.del(<ScriptRunResouce>script);
|
||||
if (script.metadata.match) {
|
||||
script.metadata.match.forEach((url) => {
|
||||
try {
|
||||
this.match.add(url, script);
|
||||
} catch (e) {
|
||||
logger.error("url load error", Logger.E(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (script.metadata.include) {
|
||||
script.metadata.include.forEach((url) => {
|
||||
try {
|
||||
this.include.add(url, script);
|
||||
} catch (e) {
|
||||
logger.error("url load error", Logger.E(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (script.metadata.exclude) {
|
||||
script.metadata.exclude.forEach((url) => {
|
||||
try {
|
||||
this.include.exclude(url, script);
|
||||
this.match.exclude(url, script);
|
||||
} catch (e) {
|
||||
logger.error("url load error", Logger.E(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (script.selfMetadata && script.selfMetadata.exclude) {
|
||||
script.selfMetadata.exclude.forEach((url) => {
|
||||
try {
|
||||
this.customizeExclude.add(url, script);
|
||||
} catch (e) {
|
||||
logger.error("url load error", Logger.E(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// 卸载页面脚本
|
||||
unloadPageScript(script: Script) {
|
||||
return this.loadPageScript(<ScriptRunResouce>script);
|
||||
}
|
||||
|
||||
// 加载并启动后台脚本
|
||||
loadBackgroundScript(script: ScriptRunResouce): Promise<boolean> {
|
||||
this.runBackScript.set(script.id, script);
|
||||
return new Promise((resolve, reject) => {
|
||||
// 清除重试数据
|
||||
script.nextruntime = 0;
|
||||
this.messageSandbox
|
||||
?.syncSend("enable", script)
|
||||
.then(() => {
|
||||
resolve(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error("backscript load error", Logger.E(err));
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 卸载并停止后台脚本
|
||||
unloadBackgroundScript(script: Script): Promise<boolean> {
|
||||
this.runBackScript.delete(script.id);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.messageSandbox
|
||||
?.syncSend("disable", script.id)
|
||||
.then(() => {
|
||||
resolve(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error("backscript stop error", Logger.E(err));
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async startBackgroundScript(script: Script) {
|
||||
const scriptRes = await this.buildScriptRunResource(script);
|
||||
this.messageSandbox?.syncSend("start", scriptRes);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
stopBackgroundScript(scriptId: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.messageSandbox
|
||||
?.syncSend("stop", scriptId)
|
||||
.then((resp) => {
|
||||
resolve(resp);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error("backscript stop error", Logger.E(err));
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async buildScriptRunResource(script: Script): Promise<ScriptRunResouce> {
|
||||
const ret: ScriptRunResouce = <ScriptRunResouce>Object.assign(script);
|
||||
|
||||
// 自定义配置
|
||||
if (ret.selfMetadata) {
|
||||
ret.metadata = { ...ret.metadata };
|
||||
Object.keys(ret.selfMetadata).forEach((key) => {
|
||||
ret.metadata[key] = ret.selfMetadata![key];
|
||||
});
|
||||
}
|
||||
|
||||
ret.value = await this.valueManager.getScriptValues(ret);
|
||||
|
||||
ret.resource = await this.resourceManager.getScriptResources(ret);
|
||||
|
||||
ret.flag = randomString(16);
|
||||
ret.sourceCode = ret.code;
|
||||
ret.code = compileScriptCode(ret);
|
||||
|
||||
ret.grantMap = {};
|
||||
|
||||
ret.metadata.grant?.forEach((val: string) => {
|
||||
ret.grantMap[val] = "ok";
|
||||
});
|
||||
|
||||
return Promise.resolve(ret);
|
||||
}
|
||||
}
|
535
src/runtime/service_worker/utils.ts
Normal file
535
src/runtime/service_worker/utils.ts
Normal file
@ -0,0 +1,535 @@
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import { Channel } from "@App/app/message/channel";
|
||||
import { SCRIPT_STATUS_ENABLE, Script } from "@App/app/repo/scripts";
|
||||
import { isFirefox } from "@App/pkg/utils/utils";
|
||||
import MessageCenter from "@App/app/message/center";
|
||||
import IoC from "@App/app/ioc";
|
||||
import { Request } from "./gm_api";
|
||||
import Runtime from "./runtime";
|
||||
|
||||
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 const responseHeaders: { [key: string]: boolean } = {
|
||||
"set-cookie": true,
|
||||
};
|
||||
|
||||
export function isUnsafeHeaders(header: string) {
|
||||
return unsafeHeaders[header.toLocaleLowerCase()];
|
||||
}
|
||||
|
||||
export function isExtensionRequest(
|
||||
details: chrome.webRequest.ResourceRequest & { originUrl?: string }
|
||||
): boolean {
|
||||
return !!(
|
||||
(details.initiator &&
|
||||
chrome.runtime.getURL("").startsWith(details.initiator)) ||
|
||||
(details.originUrl &&
|
||||
details.originUrl.startsWith(chrome.runtime.getURL("")))
|
||||
);
|
||||
}
|
||||
|
||||
// 监听web请求,处理unsafeHeaders
|
||||
export function listenerWebRequest(headerFlag: string) {
|
||||
const reqOpt = ["blocking", "requestHeaders"];
|
||||
const respOpt = ["blocking", "responseHeaders"];
|
||||
if (!isFirefox()) {
|
||||
reqOpt.push("extraHeaders");
|
||||
respOpt.push("extraHeaders");
|
||||
}
|
||||
const maxRedirects = new Map<string, [number, number]>();
|
||||
const isRedirects = new Map<string, boolean>();
|
||||
// 处理发送请求的unsafeHeaders
|
||||
chrome.webRequest.onBeforeSendHeaders.addListener(
|
||||
(details) => {
|
||||
if (!isExtensionRequest(details)) {
|
||||
return {};
|
||||
}
|
||||
// 处理unsafeHeaders
|
||||
let cookie = "";
|
||||
let setCookie = "";
|
||||
let anonymous = false;
|
||||
let isGmXhr = false;
|
||||
const requestHeaders: chrome.webRequest.HttpHeader[] = [];
|
||||
const preRequestHeaders: { [key: string]: string | null } = {};
|
||||
details.requestHeaders?.forEach((val) => {
|
||||
const lowerCase = val.name.toLowerCase();
|
||||
if (lowerCase.startsWith(`${headerFlag}-`)) {
|
||||
const headerKey = lowerCase.substring(headerFlag.length + 1);
|
||||
// 处理unsafeHeaders
|
||||
switch (headerKey) {
|
||||
case "cookie":
|
||||
setCookie = val.value || "";
|
||||
break;
|
||||
case "max-redirects":
|
||||
maxRedirects.set(details.requestId, [
|
||||
0,
|
||||
parseInt(val.value || "", 10),
|
||||
]);
|
||||
break;
|
||||
case "anonymous":
|
||||
anonymous = true;
|
||||
break;
|
||||
case "gm-xhr":
|
||||
isGmXhr = true;
|
||||
break;
|
||||
default:
|
||||
preRequestHeaders[headerKey] = val.value || null;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 原生header
|
||||
switch (lowerCase) {
|
||||
case "cookie":
|
||||
cookie = val.value || "";
|
||||
break;
|
||||
default:
|
||||
// 如果是unsafeHeaders,则判断是否已经有值,有值则不进行处理
|
||||
if (
|
||||
unsafeHeaders[lowerCase] ||
|
||||
lowerCase.startsWith("sec-") ||
|
||||
lowerCase.startsWith("proxy-")
|
||||
) {
|
||||
// null表示不发送此header
|
||||
if (preRequestHeaders[lowerCase] !== null) {
|
||||
preRequestHeaders[lowerCase] =
|
||||
preRequestHeaders[lowerCase] || val.value || "";
|
||||
}
|
||||
} else {
|
||||
requestHeaders.push(val);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
// 不是由GM XHR发起的请求,不处理
|
||||
if (!isGmXhr) {
|
||||
return {};
|
||||
}
|
||||
// 匿名移除掉cookie
|
||||
if (anonymous) {
|
||||
cookie = "";
|
||||
}
|
||||
// 有设置cookie,则进行处理
|
||||
if (setCookie) {
|
||||
// 判断结尾是否有分号,没有则添加,然后进行拼接
|
||||
if (!cookie || cookie.endsWith(";")) {
|
||||
cookie += setCookie;
|
||||
} else {
|
||||
cookie += `;${setCookie}`;
|
||||
}
|
||||
}
|
||||
// 有cookie,则进行处理
|
||||
if (cookie) {
|
||||
requestHeaders.push({
|
||||
name: "Cookie",
|
||||
value: cookie,
|
||||
});
|
||||
}
|
||||
Object.keys(preRequestHeaders).forEach((key) => {
|
||||
// null表示不发送此header
|
||||
if (preRequestHeaders[key] !== null) {
|
||||
requestHeaders.push({
|
||||
name: key,
|
||||
value: preRequestHeaders[key]!,
|
||||
});
|
||||
}
|
||||
});
|
||||
return {
|
||||
requestHeaders,
|
||||
};
|
||||
},
|
||||
{
|
||||
urls: ["<all_urls>"],
|
||||
},
|
||||
reqOpt
|
||||
);
|
||||
// 处理无法读取的responseHeaders
|
||||
chrome.webRequest.onHeadersReceived.addListener(
|
||||
(details) => {
|
||||
if (!isExtensionRequest(details)) {
|
||||
// 判断是否为页面请求
|
||||
if (
|
||||
!(details.type === "main_frame" || details.type === "sub_frame") ||
|
||||
!isFirefox()
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
// 判断页面上是否有脚本会运行,如果有判断是否有csp,有则移除csp策略
|
||||
const runtime = IoC.instance(Runtime) as Runtime;
|
||||
// 这块代码与runtime里的pageLoad一样,考虑后面要不要优化
|
||||
const result = runtime.matchUrl(details.url, (script) => {
|
||||
// 如果是iframe,判断是否允许在iframe里运行
|
||||
if (details.type === "sub_frame") {
|
||||
if (script.metadata.noframes) {
|
||||
return true;
|
||||
}
|
||||
return script.status !== SCRIPT_STATUS_ENABLE;
|
||||
}
|
||||
return script.status !== SCRIPT_STATUS_ENABLE;
|
||||
});
|
||||
if (result.length > 0 && details.responseHeaders) {
|
||||
// 移除csp
|
||||
for (let i = 0; i < details.responseHeaders.length; i += 1) {
|
||||
if (
|
||||
details.responseHeaders[i].name.toLowerCase() ===
|
||||
"content-security-policy"
|
||||
) {
|
||||
details.responseHeaders[i].value = "";
|
||||
}
|
||||
}
|
||||
return {
|
||||
responseHeaders: details.responseHeaders,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
const appendHeaders: chrome.webRequest.HttpHeader[] = [];
|
||||
details.responseHeaders?.forEach((val) => {
|
||||
const lowerCase = val.name.toLowerCase();
|
||||
if (responseHeaders[lowerCase]) {
|
||||
const copy = { ...val };
|
||||
copy.name = `${headerFlag}-${val.name}`;
|
||||
appendHeaders.push(copy);
|
||||
}
|
||||
// 处理最大重定向次数
|
||||
if (lowerCase === "location") {
|
||||
isRedirects.set(details.requestId, true);
|
||||
const nums = maxRedirects.get(details.requestId);
|
||||
if (nums) {
|
||||
nums[0] += 1;
|
||||
// 当前重定向次数大于最大重定向次数时,修改掉locatin,防止重定向
|
||||
if (nums[0] > nums[1]) {
|
||||
val.name = `${headerFlag}-${val.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
details.responseHeaders?.push(...appendHeaders);
|
||||
// 判断是否为重定向请求,如果是,将url注入到finalUrl
|
||||
if (isRedirects.has(details.requestId)) {
|
||||
details.responseHeaders?.push({
|
||||
name: `${headerFlag}-final-url`,
|
||||
value: details.url,
|
||||
});
|
||||
}
|
||||
return {
|
||||
responseHeaders: details.responseHeaders,
|
||||
};
|
||||
},
|
||||
{
|
||||
urls: ["<all_urls>"],
|
||||
},
|
||||
respOpt
|
||||
);
|
||||
chrome.webRequest.onCompleted.addListener(
|
||||
(details) => {
|
||||
if (!isExtensionRequest(details)) {
|
||||
return;
|
||||
}
|
||||
// 删除最大重定向数缓存
|
||||
maxRedirects.delete(details.requestId);
|
||||
isRedirects.delete(details.requestId);
|
||||
},
|
||||
{ urls: ["<all_urls>"] }
|
||||
);
|
||||
}
|
||||
|
||||
// 给xhr添加headers,包括unsafeHeaders
|
||||
export function setXhrHeader(
|
||||
headerFlag: string,
|
||||
config: GMSend.XHRDetails,
|
||||
xhr: XMLHttpRequest
|
||||
) {
|
||||
xhr.setRequestHeader(`${headerFlag}-gm-xhr`, "true");
|
||||
if (config.headers) {
|
||||
let hasOrigin = false;
|
||||
Object.keys(config.headers).forEach((key) => {
|
||||
const lowKey = key.toLowerCase();
|
||||
if (lowKey === "origin") {
|
||||
hasOrigin = true;
|
||||
}
|
||||
try {
|
||||
if (
|
||||
unsafeHeaders[lowKey] ||
|
||||
lowKey.startsWith("sec-") ||
|
||||
lowKey.startsWith("proxy-")
|
||||
) {
|
||||
xhr.setRequestHeader(
|
||||
`${headerFlag}-${lowKey}`,
|
||||
config.headers![key]!
|
||||
);
|
||||
} else {
|
||||
// 直接设置header
|
||||
xhr.setRequestHeader(key, config.headers![key]!);
|
||||
}
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error(
|
||||
"GM XHR setRequestHeader error"
|
||||
);
|
||||
}
|
||||
});
|
||||
if (!hasOrigin) {
|
||||
xhr.setRequestHeader(`${headerFlag}-origin`, "");
|
||||
}
|
||||
}
|
||||
if (config.maxRedirects !== undefined) {
|
||||
xhr.setRequestHeader(
|
||||
`${headerFlag}-max-redirects`,
|
||||
config.maxRedirects.toString()
|
||||
);
|
||||
}
|
||||
if (config.cookie) {
|
||||
try {
|
||||
xhr.setRequestHeader(`${headerFlag}-cookie`, config.cookie);
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error(
|
||||
"GM XHR setRequestHeader cookie error"
|
||||
);
|
||||
}
|
||||
}
|
||||
if (config.anonymous) {
|
||||
xhr.setRequestHeader(`${headerFlag}-anonymous`, "true");
|
||||
}
|
||||
}
|
||||
|
||||
export function getFetchHeader(
|
||||
headerFlag: string,
|
||||
config: GMSend.XHRDetails
|
||||
): any {
|
||||
const headers: { [key: string]: string } = {};
|
||||
headers[`${headerFlag}-gm-xhr`] = "true";
|
||||
if (config.headers) {
|
||||
Object.keys(config.headers).forEach((key) => {
|
||||
const lowKey = key.toLowerCase();
|
||||
if (
|
||||
unsafeHeaders[lowKey] ||
|
||||
lowKey.startsWith("sec-") ||
|
||||
lowKey.startsWith("proxy-")
|
||||
) {
|
||||
headers[`${headerFlag}-${lowKey}`] = config.headers![key]!;
|
||||
} else {
|
||||
// 直接设置header
|
||||
headers[key] = config.headers![key]!;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (config.maxRedirects !== undefined) {
|
||||
headers[`${headerFlag}-max-redirects`] = config.maxRedirects.toString();
|
||||
}
|
||||
if (config.cookie) {
|
||||
headers[`${headerFlag}-cookie`] = config.cookie;
|
||||
}
|
||||
if (config.anonymous) {
|
||||
headers[`${headerFlag}-anonymous`] = "true";
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export async function dealXhr(
|
||||
headerFlag: string,
|
||||
config: GMSend.XHRDetails,
|
||||
xhr: XMLHttpRequest
|
||||
): Promise<GMTypes.XHRResponse> {
|
||||
let finalUrl = xhr.responseURL || config.url;
|
||||
// 判断是否有headerFlag-final-url,有则替换finalUrl
|
||||
const finalUrlHeader = xhr.getResponseHeader(`${headerFlag}-final-url`);
|
||||
if (finalUrlHeader) {
|
||||
finalUrl = finalUrlHeader;
|
||||
}
|
||||
const removeXCat = new RegExp(`${headerFlag}-`, "g");
|
||||
const respond: GMTypes.XHRResponse = {
|
||||
finalUrl,
|
||||
readyState: <any>xhr.readyState,
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText,
|
||||
responseHeaders: xhr.getAllResponseHeaders().replace(removeXCat, ""),
|
||||
responseType: config.responseType,
|
||||
};
|
||||
if (xhr.readyState === 4) {
|
||||
if (
|
||||
config.responseType?.toLowerCase() === "arraybuffer" ||
|
||||
config.responseType?.toLowerCase() === "blob"
|
||||
) {
|
||||
let blob: Blob;
|
||||
if (xhr.response instanceof ArrayBuffer) {
|
||||
blob = new Blob([xhr.response]);
|
||||
respond.response = URL.createObjectURL(blob);
|
||||
} else {
|
||||
blob = <Blob>xhr.response;
|
||||
respond.response = URL.createObjectURL(blob);
|
||||
}
|
||||
try {
|
||||
if (xhr.getResponseHeader("Content-Type")?.indexOf("text") !== -1) {
|
||||
// 如果是文本类型,则尝试转换为文本
|
||||
respond.responseText = await blob.text();
|
||||
}
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error(
|
||||
"GM XHR getResponseHeader error"
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(<string>respond.response);
|
||||
}, 60e3);
|
||||
} else if (config.responseType === "json") {
|
||||
try {
|
||||
respond.response = JSON.parse(xhr.responseText);
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error("GM XHR JSON parse error");
|
||||
}
|
||||
try {
|
||||
respond.responseText = xhr.responseText;
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error("GM XHR getResponseText error");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
respond.response = xhr.response;
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error("GM XHR response error");
|
||||
}
|
||||
try {
|
||||
respond.responseText = xhr.responseText || undefined;
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error("GM XHR getResponseText error");
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(respond);
|
||||
}
|
||||
|
||||
export function dealFetch(
|
||||
headerFlag: string,
|
||||
config: GMSend.XHRDetails,
|
||||
response: Response,
|
||||
readyState: 0 | 1 | 2 | 3 | 4
|
||||
) {
|
||||
const removeXCat = new RegExp(`${headerFlag}-`, "g");
|
||||
let respHeader = "";
|
||||
response.headers &&
|
||||
response.headers.forEach((value, key) => {
|
||||
respHeader += `${key.replace(removeXCat, "")}: ${value}\n`;
|
||||
});
|
||||
const respond: GMTypes.XHRResponse = {
|
||||
finalUrl: response.url || config.url,
|
||||
readyState,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
responseHeaders: respHeader,
|
||||
responseType: config.responseType,
|
||||
};
|
||||
return respond;
|
||||
}
|
||||
|
||||
export function getIcon(script: Script): string {
|
||||
return (
|
||||
(script.metadata.icon && script.metadata.icon[0]) ||
|
||||
(script.metadata.iconurl && script.metadata.iconurl[0]) ||
|
||||
(script.metadata.defaulticon && script.metadata.defaulticon[0]) ||
|
||||
(script.metadata.icon64 && script.metadata.icon64[0]) ||
|
||||
(script.metadata.icon64url && script.metadata.icon64url[0])
|
||||
);
|
||||
}
|
||||
function genScriptMenuByTabMap(
|
||||
tabMap: Map<number, { request: Request; channel: Channel }[]>
|
||||
) {
|
||||
tabMap.forEach((menuArr, scriptId) => {
|
||||
// 创建脚本菜单
|
||||
chrome.contextMenus.create({
|
||||
id: `scriptMenu_${scriptId}`,
|
||||
title: menuArr[0].request.script.name,
|
||||
contexts: ["all"],
|
||||
parentId: "scriptMenu",
|
||||
});
|
||||
menuArr.forEach((menu) => {
|
||||
// 创建菜单
|
||||
chrome.contextMenus.create({
|
||||
id: `scriptMenu_menu_${scriptId}_${menu.request.params[0]}`,
|
||||
title: menu.request.params[1],
|
||||
contexts: ["all"],
|
||||
parentId: `scriptMenu_${scriptId}`,
|
||||
onclick: () => {
|
||||
(IoC.instance(MessageCenter) as MessageCenter).sendNative(
|
||||
{
|
||||
tag: menu.request.sender.targetTag,
|
||||
id: [
|
||||
menu.request.sender.frameId || menu.request.sender.tabId || 0,
|
||||
],
|
||||
},
|
||||
{
|
||||
stream: menu.channel.flag,
|
||||
channel: true,
|
||||
data: "click",
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 生成chrome菜单
|
||||
export function genScriptMenu(
|
||||
tabId: number | string,
|
||||
scriptMenu: Map<
|
||||
number | string,
|
||||
Map<
|
||||
number,
|
||||
{
|
||||
request: Request;
|
||||
channel: Channel;
|
||||
}[]
|
||||
>
|
||||
>
|
||||
) {
|
||||
// 移除之前所有的菜单
|
||||
chrome.contextMenus.removeAll();
|
||||
const tabMap = scriptMenu.get(tabId);
|
||||
const backTabMap = scriptMenu.get("sandbox");
|
||||
if (!tabMap && !backTabMap) {
|
||||
return;
|
||||
}
|
||||
// 创建根菜单
|
||||
chrome.contextMenus.create({
|
||||
id: "scriptMenu",
|
||||
title: "ScriptCat",
|
||||
contexts: ["all"],
|
||||
});
|
||||
if (tabMap) {
|
||||
genScriptMenuByTabMap(tabMap);
|
||||
}
|
||||
// 后台脚本的菜单
|
||||
if (tabId !== "sandbox") {
|
||||
if (backTabMap) {
|
||||
genScriptMenuByTabMap(backTabMap);
|
||||
}
|
||||
}
|
||||
}
|
8
src/runtime/utils.ts
Normal file
8
src/runtime/utils.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Script } from "@App/app/repo/scripts";
|
||||
|
||||
export function storageKey(script: Script): string {
|
||||
if (script.metadata.storagename) {
|
||||
return script.metadata.storagename[0];
|
||||
}
|
||||
return script.uuid;
|
||||
}
|
@ -13,6 +13,7 @@
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "bundler",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"baseUrl": ".",
|
||||
|
Loading…
x
Reference in New Issue
Block a user