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> {
|
import { Message, MessageConnect } from "./server";
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
chrome.runtime.sendMessage({ action, data }, (res) => {
|
export function sendMessage(msg: Message, action: string, data?: any): Promise<any> {
|
||||||
if (res.code) {
|
return msg.sendMessage({ action, data });
|
||||||
console.error(res);
|
|
||||||
reject(res.message);
|
|
||||||
} else {
|
|
||||||
resolve(res.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function connect(action: string, data?: any): Promise<chrome.runtime.Port> {
|
export function connect(msg: Message, action: string, data?: any): Promise<MessageConnect> {
|
||||||
return new Promise((resolve) => {
|
return msg.connect({ action, data });
|
||||||
const port = chrome.runtime.connect();
|
|
||||||
port.postMessage({ action, data });
|
|
||||||
resolve(port);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Client {
|
export class Client {
|
||||||
constructor(private prefix: string) {
|
constructor(
|
||||||
|
private msg: Message,
|
||||||
|
private prefix: string
|
||||||
|
) {
|
||||||
if (!this.prefix.endsWith("/")) {
|
if (!this.prefix.endsWith("/")) {
|
||||||
this.prefix += "/";
|
this.prefix += "/";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
do(action: string, params?: any): Promise<any> {
|
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);
|
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: {
|
parser: {
|
||||||
syntax: "typescript",
|
syntax: "typescript",
|
||||||
tsx: true,
|
tsx: true,
|
||||||
|
decorators: true,
|
||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
react: {
|
react: {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Repo } from "./repo";
|
import { Repo } from "./repo";
|
||||||
import { Resource } from "./resource";
|
import { Resource } from "./resource";
|
||||||
import { Value } from "./value";
|
|
||||||
|
|
||||||
// 脚本模型
|
// 脚本模型
|
||||||
export type SCRIPT_TYPE = 1 | 2 | 3;
|
export type SCRIPT_TYPE = 1 | 2 | 3;
|
||||||
@ -71,15 +70,19 @@ export interface ScriptCode {
|
|||||||
code: string; // 脚本执行代码
|
code: string; // 脚本执行代码
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ScriptAndCode = Script & ScriptCode;
|
||||||
|
|
||||||
// 脚本运行时的资源,包含已经编译好的脚本与脚本需要的资源
|
// 脚本运行时的资源,包含已经编译好的脚本与脚本需要的资源
|
||||||
export interface ScriptRunResouce extends Script {
|
export interface ScriptRunResouce extends Script {
|
||||||
code: string;
|
code: string;
|
||||||
value: { [key: string]: Value };
|
value: { [key: string]: any };
|
||||||
flag: string;
|
flag: string;
|
||||||
resource: { [key: string]: Resource };
|
resource: { [key: string]: Resource };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScriptDAO extends Repo<Script> {
|
export class ScriptDAO extends Repo<Script> {
|
||||||
|
scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("script");
|
super("script");
|
||||||
}
|
}
|
||||||
@ -88,6 +91,15 @@ export class ScriptDAO extends Repo<Script> {
|
|||||||
return super._save(val.uuid, val);
|
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) {
|
public findByName(name: string) {
|
||||||
return this.findOne((key, value) => {
|
return this.findOne((key, value) => {
|
||||||
return value.name === name;
|
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) {
|
public findByUUIDAndSubscribeUrl(uuid: string, suburl: string) {
|
||||||
return this.findOne((key, value) => {
|
return this.findOne((key, value) => {
|
||||||
return value.uuid === uuid && value.subscribeUrl === suburl;
|
return value.uuid === uuid && value.subscribeUrl === suburl;
|
||||||
|
@ -1,15 +1,5 @@
|
|||||||
import { Repo } from "./repo";
|
import { Repo } from "./repo";
|
||||||
|
|
||||||
export interface OldValue {
|
|
||||||
id: number;
|
|
||||||
scriptId: number;
|
|
||||||
storageName?: string;
|
|
||||||
key: string;
|
|
||||||
value: any;
|
|
||||||
createtime: number;
|
|
||||||
updatetime: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Value {
|
export interface Value {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
storageName?: string;
|
storageName?: string;
|
||||||
@ -22,4 +12,8 @@ export class ValueDAO extends Repo<Value> {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super("value");
|
super("value");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
save(key: string, value: Value) {
|
||||||
|
return super._save(key, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { WindowMessage } from "@Packages/message/window_message";
|
import { WindowMessage } from "@Packages/message/window_message";
|
||||||
import { sendMessage } from "../utils";
|
|
||||||
import { SCRIPT_RUN_STATUS } from "@App/app/repo/scripts";
|
import { SCRIPT_RUN_STATUS } from "@App/app/repo/scripts";
|
||||||
|
import { sendMessage } from "@Packages/message/client";
|
||||||
|
|
||||||
export function preparationSandbox(msg: WindowMessage) {
|
export function preparationSandbox(msg: WindowMessage) {
|
||||||
return sendMessage(msg, "preparationSandbox");
|
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 { ScriptService } from "./script";
|
||||||
import { Broker, MessageQueue } from "@Packages/message/message_queue";
|
import { Broker, MessageQueue } from "@Packages/message/message_queue";
|
||||||
import { Logger, LoggerDAO } from "@App/app/repo/logger";
|
import { Logger, LoggerDAO } from "@App/app/repo/logger";
|
||||||
@ -21,6 +21,8 @@ export class OffscreenManager {
|
|||||||
|
|
||||||
private broker: Broker = new Broker(this.extensionMessage);
|
private broker: Broker = new Broker(this.extensionMessage);
|
||||||
|
|
||||||
|
private serviceWorker = new ServiceWorkerClient(this.extensionMessage);
|
||||||
|
|
||||||
logger(data: Logger) {
|
logger(data: Logger) {
|
||||||
const dao = new LoggerDAO();
|
const dao = new LoggerDAO();
|
||||||
dao.save(data);
|
dao.save(data);
|
||||||
@ -28,12 +30,11 @@ export class OffscreenManager {
|
|||||||
|
|
||||||
preparationSandbox() {
|
preparationSandbox() {
|
||||||
// 通知初始化好环境了
|
// 通知初始化好环境了
|
||||||
const serviceWorker = new ServiceWorkerClient();
|
this.serviceWorker.preparationOffscreen();
|
||||||
serviceWorker.preparationOffscreen();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessageToServiceWorker(data: { action: string; data: any }) {
|
sendMessageToServiceWorker(data: { action: string; data: any }) {
|
||||||
return sendMessage(data.action, data.data);
|
return sendMessage(this.extensionMessage, data.action, data.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
initManager() {
|
initManager() {
|
||||||
@ -44,5 +45,7 @@ export class OffscreenManager {
|
|||||||
this.windowApi.on("sendMessageToServiceWorker", this.sendMessageToServiceWorker.bind(this));
|
this.windowApi.on("sendMessageToServiceWorker", this.sendMessageToServiceWorker.bind(this));
|
||||||
const script = new ScriptService(group.group("script"), this.mq, this.windowMessage, this.broker);
|
const script = new ScriptService(group.group("script"), this.mq, this.windowMessage, this.broker);
|
||||||
script.init();
|
script.init();
|
||||||
|
// 转发gm api请求
|
||||||
|
forwardMessage("serviceWorker/runtime/gmApi", this.windowApi, this.extensionMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
|
import { sendMessage } from "@Packages/message/client";
|
||||||
import { WindowMessage } from "@Packages/message/window_message";
|
import { WindowMessage } from "@Packages/message/window_message";
|
||||||
import { sendMessage } from "../utils";
|
|
||||||
|
|
||||||
export function enableScript(msg: WindowMessage, data: ScriptRunResouce) {
|
export function enableScript(msg: WindowMessage, data: ScriptRunResouce) {
|
||||||
return sendMessage(msg, "enableScript", data);
|
return sendMessage(msg, "enableScript", data);
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import { Server } from "@Packages/message/server";
|
import { Server } from "@Packages/message/server";
|
||||||
import { WindowMessage } from "@Packages/message/window_message";
|
import { WindowMessage } from "@Packages/message/window_message";
|
||||||
import { preparationSandbox } from "../offscreen/client";
|
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";
|
import { Runtime } from "./runtime";
|
||||||
|
|
||||||
// sandbox环境的管理器
|
// sandbox环境的管理器
|
||||||
|
@ -107,7 +107,7 @@ export class Runtime {
|
|||||||
// 暂未实现执行完成后立马释放,会在下一次执行时释放
|
// 暂未实现执行完成后立马释放,会在下一次执行时释放
|
||||||
await this.stopScript(script.uuid);
|
await this.stopScript(script.uuid);
|
||||||
}
|
}
|
||||||
const exec = new BgExecScriptWarp(script);
|
const exec = new BgExecScriptWarp(script, this.windowMessage);
|
||||||
this.execScripts.set(script.uuid, exec);
|
this.execScripts.set(script.uuid, exec);
|
||||||
proxyUpdateRunStatus(this.windowMessage, { uuid: script.uuid, runStatus: SCRIPT_RUN_STATUS_RUNNING });
|
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 { InstallSource } from ".";
|
||||||
import { Broker } from "@Packages/message/message_queue";
|
import { Broker } from "@Packages/message/message_queue";
|
||||||
import { Resource } from "@App/app/repo/resource";
|
import { Resource } from "@App/app/repo/resource";
|
||||||
|
import { Message } from "@Packages/message/server";
|
||||||
|
|
||||||
export class ServiceWorkerClient extends Client {
|
export class ServiceWorkerClient extends Client {
|
||||||
constructor() {
|
constructor(msg: Message) {
|
||||||
super("serviceWorker");
|
super(msg, "serviceWorker");
|
||||||
}
|
}
|
||||||
|
|
||||||
preparationOffscreen() {
|
preparationOffscreen() {
|
||||||
@ -15,8 +16,8 @@ export class ServiceWorkerClient extends Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ScriptClient extends Client {
|
export class ScriptClient extends Client {
|
||||||
constructor() {
|
constructor(msg: Message) {
|
||||||
super("serviceWorker/script");
|
super(msg, "serviceWorker/script");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取安装信息
|
// 获取安装信息
|
||||||
@ -50,8 +51,8 @@ export class ScriptClient extends Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ResourceClient extends Client {
|
export class ResourceClient extends Client {
|
||||||
constructor() {
|
constructor(msg: Message) {
|
||||||
super("serviceWorker/resource");
|
super(msg, "serviceWorker/resource");
|
||||||
}
|
}
|
||||||
|
|
||||||
getScriptResources(script: Script): Promise<{ [key: string]: Resource }> {
|
getScriptResources(script: Script): Promise<{ [key: string]: Resource }> {
|
||||||
@ -60,8 +61,8 @@ export class ResourceClient extends Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ValueClient extends Client {
|
export class ValueClient extends Client {
|
||||||
constructor() {
|
constructor(msg: Message) {
|
||||||
super("serviceWorker/value");
|
super(msg, "serviceWorker/value");
|
||||||
}
|
}
|
||||||
|
|
||||||
getScriptValue(script: Script) {
|
getScriptValue(script: Script) {
|
||||||
|
@ -29,7 +29,7 @@ export default class ServiceWorkerManager {
|
|||||||
value.init();
|
value.init();
|
||||||
const script = new ScriptService(group.group("script"), this.mq, value, resource);
|
const script = new ScriptService(group.group("script"), this.mq, value, resource);
|
||||||
script.init();
|
script.init();
|
||||||
const runtime = new RuntimeService(group.group("runtime"), this.mq);
|
const runtime = new RuntimeService(group.group("runtime"), this.mq, value);
|
||||||
runtime.init();
|
runtime.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,23 @@
|
|||||||
import { MessageQueue } from "@Packages/message/message_queue";
|
import { MessageQueue } from "@Packages/message/message_queue";
|
||||||
import { ScriptEnableCallbackValue } from "./client";
|
import { ScriptEnableCallbackValue } from "./client";
|
||||||
import { Group } from "@Packages/message/server";
|
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 {
|
export class RuntimeService {
|
||||||
scriptDAO: ScriptDAO = new ScriptDAO();
|
scriptDAO: ScriptDAO = new ScriptDAO();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private group: Group,
|
private group: Group,
|
||||||
private mq: MessageQueue
|
private mq: MessageQueue,
|
||||||
|
private value: ValueService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
// 监听脚本开启
|
// 监听脚本开启
|
||||||
this.mq.addListener("enableScript", async (data: ScriptEnableCallbackValue) => {
|
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) {
|
if (!script) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -48,9 +51,17 @@ export class RuntimeService {
|
|||||||
this.mq.publish("enableScript", { uuid: script.uuid, enable: true });
|
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) {}
|
unregistryPageScript(script: Script) {}
|
||||||
}
|
}
|
||||||
|
@ -11,14 +11,12 @@ import {
|
|||||||
SCRIPT_RUN_STATUS,
|
SCRIPT_RUN_STATUS,
|
||||||
SCRIPT_STATUS_DISABLE,
|
SCRIPT_STATUS_DISABLE,
|
||||||
SCRIPT_STATUS_ENABLE,
|
SCRIPT_STATUS_ENABLE,
|
||||||
SCRIPT_TYPE_NORMAL,
|
|
||||||
ScriptCodeDAO,
|
ScriptCodeDAO,
|
||||||
ScriptDAO,
|
ScriptDAO,
|
||||||
ScriptRunResouce,
|
ScriptRunResouce,
|
||||||
} from "@App/app/repo/scripts";
|
} from "@App/app/repo/scripts";
|
||||||
import { MessageQueue } from "@Packages/message/message_queue";
|
import { MessageQueue } from "@Packages/message/message_queue";
|
||||||
import { InstallSource } from ".";
|
import { InstallSource } from ".";
|
||||||
import { ScriptEnableCallbackValue } from "./client";
|
|
||||||
import { ResourceService } from "./resource";
|
import { ResourceService } from "./resource";
|
||||||
import { ValueService } from "./value";
|
import { ValueService } from "./value";
|
||||||
import { compileScriptCode } from "@App/runtime/content/utils";
|
import { compileScriptCode } from "@App/runtime/content/utils";
|
||||||
@ -163,23 +161,26 @@ export class ScriptService {
|
|||||||
upsertBy,
|
upsertBy,
|
||||||
});
|
});
|
||||||
let update = false;
|
let update = false;
|
||||||
const dao = new ScriptDAO();
|
|
||||||
// 判断是否已经安装
|
// 判断是否已经安装
|
||||||
const oldScript = await dao.findByUUID(script.uuid);
|
const oldScript = await this.scriptDAO.get(script.uuid);
|
||||||
if (oldScript) {
|
if (oldScript) {
|
||||||
// 执行更新逻辑
|
// 执行更新逻辑
|
||||||
update = true;
|
update = true;
|
||||||
script.selfMetadata = oldScript.selfMetadata;
|
script.selfMetadata = oldScript.selfMetadata;
|
||||||
}
|
}
|
||||||
return dao
|
return this.scriptDAO
|
||||||
.save(script)
|
.save(script)
|
||||||
.then(() => {
|
.then(async () => {
|
||||||
|
await this.scriptCodeDAO.save({
|
||||||
|
uuid: script.uuid,
|
||||||
|
code: param.code,
|
||||||
|
});
|
||||||
logger.info("install success");
|
logger.info("install success");
|
||||||
// 广播一下
|
// 广播一下
|
||||||
this.mq.publish("installScript", { script, update });
|
this.mq.publish("installScript", { script, update });
|
||||||
return {};
|
return {};
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e: any) => {
|
||||||
logger.error("install error", Logger.E(e));
|
logger.error("install error", Logger.E(e));
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
@ -187,13 +188,12 @@ export class ScriptService {
|
|||||||
|
|
||||||
async deleteScript(uuid: string) {
|
async deleteScript(uuid: string) {
|
||||||
const logger = this.logger.with({ uuid });
|
const logger = this.logger.with({ uuid });
|
||||||
const dao = new ScriptDAO();
|
const script = await this.scriptDAO.get(uuid);
|
||||||
const script = await dao.findByUUID(uuid);
|
|
||||||
if (!script) {
|
if (!script) {
|
||||||
logger.error("script not found");
|
logger.error("script not found");
|
||||||
throw new Error("script not found");
|
throw new Error("script not found");
|
||||||
}
|
}
|
||||||
return dao
|
return this.scriptDAO
|
||||||
.delete(uuid)
|
.delete(uuid)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info("delete success");
|
logger.info("delete success");
|
||||||
@ -208,13 +208,12 @@ export class ScriptService {
|
|||||||
|
|
||||||
async enableScript(param: { uuid: string; enable: boolean }) {
|
async enableScript(param: { uuid: string; enable: boolean }) {
|
||||||
const logger = this.logger.with({ uuid: param.uuid, enable: param.enable });
|
const logger = this.logger.with({ uuid: param.uuid, enable: param.enable });
|
||||||
const dao = new ScriptDAO();
|
const script = await this.scriptDAO.get(param.uuid);
|
||||||
const script = await dao.findByUUID(param.uuid);
|
|
||||||
if (!script) {
|
if (!script) {
|
||||||
logger.error("script not found");
|
logger.error("script not found");
|
||||||
throw new 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 })
|
.update(param.uuid, { status: param.enable ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info("enable success");
|
logger.info("enable success");
|
||||||
@ -228,7 +227,7 @@ export class ScriptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchInfo(uuid: string) {
|
async fetchInfo(uuid: string) {
|
||||||
const script = await new ScriptDAO().findByUUID(uuid);
|
const script = await this.scriptDAO.get(uuid);
|
||||||
if (!script) {
|
if (!script) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -236,7 +235,7 @@ export class ScriptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateRunStatus(params: { uuid: string; runStatus: SCRIPT_RUN_STATUS; error?: string; nextruntime?: number }) {
|
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,
|
runStatus: params.runStatus,
|
||||||
lastruntime: new Date().getTime(),
|
lastruntime: new Date().getTime(),
|
||||||
error: params.error,
|
error: params.error,
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import LoggerCore from "@App/app/logger/core";
|
import LoggerCore from "@App/app/logger/core";
|
||||||
import Logger from "@App/app/logger/logger";
|
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 { ValueDAO } from "@App/app/repo/value";
|
||||||
|
import { storageKey } from "@App/runtime/utils";
|
||||||
import { MessageQueue } from "@Packages/message/message_queue";
|
import { MessageQueue } from "@Packages/message/message_queue";
|
||||||
import { Group } from "@Packages/message/server";
|
import { Group } from "@Packages/message/server";
|
||||||
|
|
||||||
export class ValueService {
|
export class ValueService {
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
scriptDAO: ScriptDAO = new ScriptDAO();
|
||||||
valueDAO: ValueDAO = new ValueDAO();
|
valueDAO: ValueDAO = new ValueDAO();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -16,21 +18,37 @@ export class ValueService {
|
|||||||
this.logger = LoggerCore.logger().with({ service: "value" });
|
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) {
|
async getScriptValue(script: Script) {
|
||||||
const ret = await this.valueDAO.get(this.storageKey(script));
|
const ret = await this.valueDAO.get(storageKey(script));
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return Promise.resolve(ret?.data);
|
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() {
|
init() {
|
||||||
this.group.on("getScriptValue", this.getScriptValue.bind(this));
|
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 React, { useEffect, useState } from "react";
|
||||||
import MessageInternal from "@App/app/message/internal";
|
import MessageInternal from "@App/app/message/internal";
|
||||||
import { MessageSender } from "@App/app/message/message";
|
import { MessageSender } from "@App/app/message/message";
|
||||||
import { ScriptMenu } from "@App/runtime/background/runtime";
|
import { ScriptMenu } from "@App/runtime/service_worker/runtime";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Collapse,
|
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 old: Script | undefined;
|
||||||
let oldCode: string | undefined;
|
let oldCode: string | undefined;
|
||||||
if (uuid) {
|
if (uuid) {
|
||||||
old = await dao.findByUUID(uuid);
|
old = await dao.get(uuid);
|
||||||
if (!old && override) {
|
if (!old && override) {
|
||||||
old = await dao.findByNameAndNamespace(script.name, script.namespace);
|
old = await dao.findByNameAndNamespace(script.name, script.namespace);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import LoggerCore from "@App/app/logger/core";
|
import LoggerCore from "@App/app/logger/core";
|
||||||
import Logger from "@App/app/logger/logger";
|
import Logger from "@App/app/logger/logger";
|
||||||
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
import { Value } from "@App/app/repo/value";
|
|
||||||
import GMApi from "./gm_api";
|
import GMApi from "./gm_api";
|
||||||
import { compileScript, createContext, proxyContext, ScriptFunc } from "./utils";
|
import { compileScript, createContext, proxyContext, ScriptFunc } from "./utils";
|
||||||
|
import { Message } from "@Packages/message/server";
|
||||||
|
|
||||||
export type ValueUpdateData = {
|
export type ValueUpdateData = {
|
||||||
oldValue: any;
|
oldValue: any;
|
||||||
value: Value;
|
value: any;
|
||||||
|
key: string; // 值key
|
||||||
|
uuid: string;
|
||||||
|
storageKey: string; // 储存key
|
||||||
sender: {
|
sender: {
|
||||||
runFlag: string;
|
runFlag: string;
|
||||||
tabId?: number;
|
tabId?: number;
|
||||||
@ -30,7 +33,7 @@ export default class ExecScript {
|
|||||||
|
|
||||||
GM_info: any;
|
GM_info: any;
|
||||||
|
|
||||||
constructor(scriptRes: ScriptRunResouce, thisContext?: { [key: string]: any }) {
|
constructor(scriptRes: ScriptRunResouce, message: Message, thisContext?: { [key: string]: any }) {
|
||||||
this.scriptRes = scriptRes;
|
this.scriptRes = scriptRes;
|
||||||
this.logger = LoggerCore.getInstance().logger({
|
this.logger = LoggerCore.getInstance().logger({
|
||||||
component: "exec",
|
component: "exec",
|
||||||
@ -49,7 +52,7 @@ export default class ExecScript {
|
|||||||
this.proxyContent = global;
|
this.proxyContent = global;
|
||||||
} else {
|
} else {
|
||||||
// 构建脚本GM上下文
|
// 构建脚本GM上下文
|
||||||
this.sandboxContent = createContext(scriptRes, this.GM_info);
|
this.sandboxContent = createContext(scriptRes, this.GM_info, message);
|
||||||
this.proxyContent = proxyContext(global, this.sandboxContent, thisContext);
|
this.proxyContent = proxyContext(global, this.sandboxContent, thisContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
import ExecScript from "./exec_script";
|
import ExecScript from "./exec_script";
|
||||||
|
import { Message } from "@Packages/message/server";
|
||||||
|
|
||||||
export class CATRetryError {
|
export class CATRetryError {
|
||||||
msg: string;
|
msg: string;
|
||||||
@ -21,7 +22,7 @@ export class BgExecScriptWarp extends ExecScript {
|
|||||||
|
|
||||||
setInterval: Map<number, boolean>;
|
setInterval: Map<number, boolean>;
|
||||||
|
|
||||||
constructor(scriptRes: ScriptRunResouce) {
|
constructor(scriptRes: ScriptRunResouce, message: Message) {
|
||||||
const thisContext: { [key: string]: any } = {};
|
const thisContext: { [key: string]: any } = {};
|
||||||
const setTimeout = new Map<number, any>();
|
const setTimeout = new Map<number, any>();
|
||||||
const setInterval = new Map<number, any>();
|
const setInterval = new Map<number, any>();
|
||||||
@ -62,7 +63,7 @@ export class BgExecScriptWarp extends ExecScript {
|
|||||||
};
|
};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
thisContext.CATRetryError = CATRetryError;
|
thisContext.CATRetryError = CATRetryError;
|
||||||
super(scriptRes, thisContext);
|
super(scriptRes, message, thisContext);
|
||||||
this.setTimeout = setTimeout;
|
this.setTimeout = setTimeout;
|
||||||
this.setInterval = setInterval;
|
this.setInterval = setInterval;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
import { getMetadataStr, getUserConfigStr, parseUserConfig } from "@App/pkg/utils/script";
|
import { getMetadataStr, getUserConfigStr, parseUserConfig } from "@App/pkg/utils/script";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import { ValueUpdateData } from "./exec_script";
|
import { ValueUpdateData } from "./exec_script";
|
||||||
import { ExtVersion } from "@App/app/const";
|
import { ExtVersion } from "@App/app/const";
|
||||||
|
import { storageKey } from "../utils";
|
||||||
|
import { Message } from "@Packages/message/server";
|
||||||
|
|
||||||
interface ApiParam {
|
interface ApiParam {
|
||||||
depend?: string[];
|
depend?: string[];
|
||||||
@ -57,34 +58,44 @@ export default class GMApi {
|
|||||||
|
|
||||||
valueChangeListener = new Map<number, { name: string; listener: GMTypes.ValueChangeListener }>();
|
valueChangeListener = new Map<number, { name: string; listener: GMTypes.ValueChangeListener }>();
|
||||||
|
|
||||||
|
constructor(private message: Message) {}
|
||||||
|
|
||||||
// 单次回调使用
|
// 单次回调使用
|
||||||
public sendMessage(api: string, params: any[]) {
|
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[]) {
|
public connect(api: string, params: any[]) {
|
||||||
return null;
|
return this.message.connect({
|
||||||
|
action: "serviceWorker/runtime/gmApi",
|
||||||
|
data: {
|
||||||
|
api,
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public valueUpdate(data: ValueUpdateData) {
|
public valueUpdate(data: ValueUpdateData) {
|
||||||
const { storagename } = this.scriptRes.metadata;
|
if (data.uuid === this.scriptRes.uuid || data.storageKey === storageKey(this.scriptRes)) {
|
||||||
if (
|
|
||||||
data.value.uuid === this.scriptRes.uuid ||
|
|
||||||
(storagename && data.value.storageName && storagename[0] === data.value.storageName)
|
|
||||||
) {
|
|
||||||
// 触发,并更新值
|
// 触发,并更新值
|
||||||
if (data.value.value === undefined) {
|
if (data.value === undefined) {
|
||||||
delete this.scriptRes.value[data.value.key];
|
delete this.scriptRes.value[data.value];
|
||||||
} else {
|
} else {
|
||||||
this.scriptRes.value[data.value.key] = data.value;
|
this.scriptRes.value[data.key] = data.value;
|
||||||
}
|
}
|
||||||
this.valueChangeListener.forEach((item) => {
|
this.valueChangeListener.forEach((item) => {
|
||||||
if (item.name === data.value.key) {
|
if (item.name === data.value.key) {
|
||||||
item.listener(
|
item.listener(
|
||||||
data.value.key,
|
data.value.key,
|
||||||
data.oldValue,
|
data.oldValue,
|
||||||
data.value.value,
|
data.value,
|
||||||
data.sender.runFlag !== this.runFlag,
|
data.sender.runFlag !== this.runFlag,
|
||||||
data.sender.tabId
|
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 { v4 as uuidv4 } from "uuid";
|
||||||
import GMApi, { ApiValue, GMContext } from "./gm_api";
|
import GMApi, { ApiValue, GMContext } from "./gm_api";
|
||||||
import { has } from "@App/pkg/utils/lodash";
|
import { has } from "@App/pkg/utils/lodash";
|
||||||
|
import { Message } from "@Packages/message/server";
|
||||||
|
|
||||||
// 构建脚本运行代码
|
// 构建脚本运行代码
|
||||||
export function compileScriptCode(scriptRes: ScriptRunResouce, code: string): string {
|
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构建
|
// 按照GMApi构建
|
||||||
const context: { [key: string]: any } = {
|
const context: { [key: string]: any } = {
|
||||||
|
message: message,
|
||||||
scriptRes,
|
scriptRes,
|
||||||
valueChangeListener: new Map<number, { name: string; listener: GMTypes.ValueChangeListener }>(),
|
valueChangeListener: new Map<number, { name: string; listener: GMTypes.ValueChangeListener }>(),
|
||||||
sendMessage: GMApi.prototype.sendMessage,
|
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,
|
"isolatedModules": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
"experimentalDecorators": true,
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user