gm api通信
Some checks failed
test / Run tests (push) Failing after 9s
build / Build (push) Failing after 16s

This commit is contained in:
王一之 2025-01-26 15:39:14 +08:00
parent 415f00a3d1
commit 9f8f7c8347
29 changed files with 1941 additions and 112 deletions

View File

@ -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);
} }
} }

View File

@ -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 });
}
});
}

View File

@ -77,6 +77,7 @@ export default defineConfig({
parser: { parser: {
syntax: "typescript", syntax: "typescript",
tsx: true, tsx: true,
decorators: true,
}, },
transform: { transform: {
react: { react: {

View File

@ -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;

View File

@ -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);
}
} }

View File

@ -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");

View File

@ -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);
} }
} }

View File

@ -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);

View File

@ -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环境的管理器

View File

@ -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 });
// 修改掉脚本掉最后运行时间, 数据库也需要修改 // 修改掉脚本掉最后运行时间, 数据库也需要修改

View File

@ -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) {

View File

@ -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();
} }
} }

View File

@ -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) {}
} }

View File

@ -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,

View File

@ -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));
} }

View File

@ -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,
});
}

View File

@ -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
View 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;
}
});
}
}

View File

@ -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);
} }

View File

@ -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);
} }
} }

View File

@ -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;
} }

View File

@ -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]);
}
} }

View File

@ -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,

View 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() {}
}

View 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}`),
});
});
}
}

View 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);
}
}

View 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
View 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;
}

View File

@ -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": ".",