处理后台脚本API

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

View File

@ -1,33 +0,0 @@
// ==UserScript==
// @name gm value
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1.0
// @description 可以持久化存储数据, 并且可以监听数据变化
// @author You
// @match https://bbs.tampermonkey.net.cn/
// @run-at document-start
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addValueChangeListener
// @grant GM_listValues
// @grant GM_deleteValue
// ==/UserScript==
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote, tabid) {
console.log("test_set change", name, oldval, newval, remote, tabid);
});
setInterval(() => {
console.log(GM_getValue("test_set"));
console.log(GM_listValues());
}, 2000);
setTimeout(() => {
GM_deleteValue("test_set");
}, 3000);
GM_setValue("test_set", new Date().getTime());
console.log(GM_getValue("test_set2"));
GM_setValue("test_set2", new Date().getTime());

View File

@ -0,0 +1,17 @@
// ==UserScript==
// @name gm value storage 设置方 - 定时脚本
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1.0
// @description 多个脚本之间共享数据 设置方 - 定时脚本
// @author You
// @run-at document-start
// @grant GM_setValue
// @grant GM_deleteValue
// @storageName example
// @crontab */5 * * * * *
// ==/UserScript==
return new Promise((resolve) => {
GM_setValue("test_set", new Date().getTime());
resolve();
});

View File

@ -23,6 +23,6 @@ GM_addValueChangeListener("test_set", function (name, oldval, newval, remote, ta
});
setInterval(() => {
console.log(GM_getValue("test_set"));
console.log(GM_listValues());
console.log("test_set: ", GM_getValue("test_set"));
console.log("value list:", GM_listValues());
}, 2000);

View File

@ -0,0 +1,32 @@
// ==UserScript==
// @name gm value storage 读取与监听方 - 后台脚本
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1.0
// @description 多个脚本之间共享数据 读取与监听方 - 后台脚本
// @author You
// @run-at document-start
// @grant GM_getValue
// @grant GM_addValueChangeListener
// @grant GM_listValues
// @grant GM_cookie
// @storageName example
// @background
// ==/UserScript==
return new Promise((resolve) => {
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote, tabid) {
console.log("value change", name, oldval, newval, remote, tabid);
// 可以通过tabid获取到触发变化的tab
// GM_cookie.store可以获取到对应的cookie storeId
GM_cookie("store", tabid, (storeId) => {
console.log("store", storeId);
});
});
setInterval(() => {
console.log("test_set: ", GM_getValue("test_set"));
console.log("value list:", GM_listValues());
}, 2000);
// 永不返回resolve表示永不结束
// resolve()
});

View File

@ -72,8 +72,8 @@ export class Server {
}
}
private messageHandle(msg: string, params: any, sendResponse: (response: any) => void, sender?: MessageSender) {
const func = this.apiFunctionMap.get(msg);
private messageHandle(action: string, params: any, sendResponse: (response: any) => void, sender?: MessageSender) {
const func = this.apiFunctionMap.get(action);
if (func) {
try {
const ret = func(params, new GetSender(sender!));
@ -89,8 +89,8 @@ export class Server {
sendResponse({ code: -1, message: e.message });
}
} else {
sendResponse({ code: -1, message: "no such api" });
this.logger.error("no such api", { msg });
sendResponse({ code: -1, message: "no such api " + action });
this.logger.error("no such api", { action: action });
}
}
}

View File

@ -135,16 +135,39 @@ export default class Cache {
return this.storage.list();
}
private txPromise: Map<string, Promise<any>> = new Map();
private txLock: Map<string, ((unlock: () => void) => void)[]> = new Map();
lock(key: string): Promise<() => void> | (() => void) {
let hasLock = this.txLock.has(key);
const unlock = () => {
let waitFunc = this.txLock.get(key)?.shift();
if (waitFunc) {
waitFunc(unlock);
} else {
this.txLock.delete(key);
}
};
if (hasLock) {
let lock = this.txLock.get(key);
if (!lock) {
lock = [];
this.txLock.set(key, lock);
}
return new Promise<() => void>((resolve) => {
lock.push(resolve);
});
}
this.txLock.set(key, []);
return unlock;
}
// 事务处理,如果有事务正在进行,则等待
public async tx<T>(key: string, set: (result: T) => Promise<T>): Promise<T> {
let promise = this.txPromise.get(key);
if (promise) {
await promise;
}
const unlock = await this.lock(key);
let newValue: T;
promise = this.get(key)
await this.get(key)
.then((result) => set(result))
.then((value) => {
if (value) {
@ -153,9 +176,7 @@ export default class Cache {
}
return Promise.resolve();
});
this.txPromise.set(key, promise);
await promise;
this.txPromise.delete(key);
unlock();
return newValue!;
}
}

View File

@ -12,9 +12,9 @@ export default class ContentRuntime {
) {}
start(scripts: ScriptRunResouce[]) {
this.extServer.on("runtime/menuClick", (data) => {
this.extServer.on("runtime/emitEvent", (data) => {
// 转发给inject
return sendMessage(this.msg, "inject/runtime/menuClick", data);
return sendMessage(this.msg, "inject/runtime/emitEvent", data);
});
this.extServer.on("runtime/valueUpdate", (data) => {
// 转发给inject

View File

@ -69,13 +69,16 @@ export default class ExecScript {
}
}
// 触发值更新
valueUpdate(data: ValueUpdateData) {
this.sandboxContent?.valueUpdate(data);
emitEvent(event: string, data: any) {
switch (event) {
case "menuClick":
this.sandboxContent?.menuClick(data);
break;
}
}
menuClick(id: number) {
this.sandboxContent?.menuClick(id);
valueUpdate(data: ValueUpdateData) {
this.sandboxContent?.valueUpdate(data);
}
exec() {

View File

@ -2,12 +2,12 @@ import { ScriptRunResouce } from "@App/app/repo/scripts";
import { getMetadataStr, getUserConfigStr, parseUserConfig } from "@App/pkg/utils/script";
import { ValueUpdateData } from "./exec_script";
import { ExtVersion } from "@App/app/const";
import { getStorageName } from "../utils";
import { Message, MessageConnect } from "@Packages/message/server";
import { CustomEventMessage } from "@Packages/message/custom_event_message";
import LoggerCore from "@App/app/logger/core";
import { connect, sendMessage } from "@Packages/message/client";
import EventEmitter from "eventemitter3";
import { getStorageName } from "@App/pkg/utils/utils";
interface ApiParam {
depend?: string[];
@ -174,17 +174,17 @@ export default class GMApi {
this.GM_setValue(name, undefined);
}
valueChangeId: number | undefined;
eventId: number = 0;
menuMap: Map<number, string> | undefined;
EE: EventEmitter = new EventEmitter();
@GMContext.API()
public GM_addValueChangeListener(name: string, listener: GMTypes.ValueChangeListener): number {
if (!this.valueChangeId) {
this.valueChangeId = 1;
} else {
this.valueChangeId += 1;
}
this.valueChangeListener.set(this.valueChangeId, { name, listener });
return this.valueChangeId;
this.eventId += 1;
this.valueChangeListener.set(this.eventId, { name, listener });
return this.eventId;
}
@GMContext.API()
@ -222,12 +222,6 @@ export default class GMApi {
return (<CustomEventMessage>this.message).getAndDelRelatedTarget(data.relatedTarget);
}
menuId: number | undefined;
menuMap: Map<number, string> | undefined;
EE: EventEmitter = new EventEmitter();
@GMContext.API()
GM_registerMenuCommand(name: string, listener: () => void, accessKey?: string): number {
if (!this.menuMap) {
@ -240,15 +234,10 @@ export default class GMApi {
}
});
if (flag) {
this.EE.addListener("menuClick" + flag, listener);
return flag;
}
if (!this.menuId) {
this.menuId = 1;
} else {
this.menuId += 1;
}
const id = this.menuId;
this.eventId += 1;
const id = this.eventId;
this.menuMap.set(id, name);
this.EE.addListener("menuClick" + id, listener);
this.sendMessage("GM_registerMenuCommand", [id, name, accessKey]);
@ -257,6 +246,7 @@ export default class GMApi {
@GMContext.API()
GM_unregisterMenuCommand(id: number): void {
console.log("unregisterMenuCommand", id);
if (!this.menuMap) {
this.menuMap = new Map();
}
@ -458,4 +448,68 @@ export default class GMApi {
},
};
}
@GMContext.API()
public async GM_notification(
detail: GMTypes.NotificationDetails | string,
ondone?: GMTypes.NotificationOnDone | string,
image?: string,
onclick?: GMTypes.NotificationOnClick
) {
let data: GMTypes.NotificationDetails = {};
if (typeof detail === "string") {
data.text = detail;
switch (arguments.length) {
case 4:
data.onclick = onclick;
case 3:
data.image = image;
case 2:
data.title = <string>ondone;
default:
break;
}
} else {
data = detail;
data.ondone = data.ondone || <GMTypes.NotificationOnDone>ondone;
}
let click: GMTypes.NotificationOnClick;
let done: GMTypes.NotificationOnDone;
let create: GMTypes.NotificationOnClick;
if (data.onclick) {
click = data.onclick;
delete data.onclick;
}
if (data.ondone) {
done = data.ondone;
delete data.ondone;
}
if (data.oncreate) {
create = data.oncreate;
delete data.oncreate;
}
this.eventId += 1;
this.sendMessage("GM_notification", [data]);
this.EE.addListener("GM_notification:" + this.eventId, (resp: any) => {
switch (resp.event) {
case "click": {
click && click.apply({ id: resp.id }, [resp.id, resp.index]);
break;
}
case "done": {
done && done.apply({ id: resp.id }, [resp.user]);
break;
}
case "create": {
create && create.apply({ id: resp.id }, [resp.id]);
break;
}
default:
LoggerCore.logger().warn("GM_notification resp is error", {
resp,
});
break;
}
});
}
}

View File

@ -2,7 +2,8 @@ import { ScriptRunResouce } from "@App/app/repo/scripts";
import { Message, Server } from "@Packages/message/server";
import ExecScript, { ValueUpdateData } from "./exec_script";
import { addStyle, ScriptFunc } from "./utils";
import { getStorageName } from "../utils";
import { getStorageName } from "@App/pkg/utils/utils";
import { EmitEventRequest } from "../service_worker/runtime";
export class InjectRuntime {
execList: ExecScript[] = [];
@ -29,11 +30,11 @@ export class InjectRuntime {
});
}
});
this.server.on("runtime/menuClick", (data: { id: number; uuid: string }) => {
this.server.on("runtime/emitEvent", (data: EmitEventRequest) => {
// 转发给脚本
const exec = this.execList.find((val) => val.scriptRes.uuid === data.uuid);
if (exec) {
exec.menuClick(data.id);
exec.emitEvent(data.event, data.data);
}
});
this.server.on("runtime/valueUpdate", (data: ValueUpdateData) => {

View File

@ -64,6 +64,7 @@ export function createContext(scriptRes: ScriptRunResouce, GMInfo: any, envPrefi
sendMessage: GMApi.prototype.sendMessage,
connect: GMApi.prototype.connect,
runFlag: uuidv4(),
eventId: 10000,
valueUpdate: GMApi.prototype.valueUpdate,
menuClick: GMApi.prototype.menuClick,
EE: new EventEmitter(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,8 @@ import MessageWriter from "./app/logger/message_writer";
import { ExtensionMessage, ExtensionMessageSend } from "@Packages/message/extension_message";
import { CustomEventMessage } from "@Packages/message/custom_event_message";
import { RuntimeClient } from "./app/service/service_worker/client";
import ContentRuntime from "./runtime/content/content";
import { Server } from "@Packages/message/server";
import ContentRuntime from "./app/service/content/content";
// 建立与service_worker页面的连接
const send = new ExtensionMessageSend();

View File

@ -2,8 +2,8 @@ import LoggerCore from "./app/logger/core";
import MessageWriter from "./app/logger/message_writer";
import { CustomEventMessage } from "@Packages/message/custom_event_message";
import { Server } from "@Packages/message/server";
import { InjectRuntime } from "./runtime/content/inject";
import { ScriptRunResouce } from "./app/repo/scripts";
import { InjectRuntime } from "./app/service/content/inject";
const msg = new CustomEventMessage(MessageFlag, false);

View File

@ -1,9 +1,11 @@
import dts from "@App/types/scriptcat.d.ts";
import dts from "@App/template/scriptcat.d.tpl";
import { languages } from "monaco-editor";
// 注册eslint
// const linterWorker = new Worker("/src/linter.worker.js");
console.log(dts, dts.length);
export default function registerEditor() {
window.MonacoEnvironment = {
getWorkerUrl(moduleId: any, label: any) {
@ -14,7 +16,7 @@ export default function registerEditor() {
},
};
languages.typescript.javascriptDefaults.addExtraLib(dts, "tampermonkey.d.ts");
languages.typescript.javascriptDefaults.addExtraLib(dts, "scriptcat.d.ts");
// 悬停提示
const prompt: { [key: string]: any } = {

View File

@ -1,4 +1,4 @@
import { Metadata } from "@App/app/repo/scripts";
import { Metadata, Script } from "@App/app/repo/scripts";
import { CronTime } from "cron";
import dayjs from "dayjs";
import semver from "semver";
@ -217,3 +217,10 @@ export function sleep(time: number) {
setTimeout(resolve, time);
});
}
export function getStorageName(script: Script): string {
if (script.metadata && script.metadata.storagename) {
return script.metadata.storagename[0];
}
return script.uuid;
}

View File

@ -1,735 +0,0 @@
// 脚本运行时,主要负责脚本的加载和匹配
// 油猴脚本将监听页面的创建,将代码注入到页面中
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.uuid);
if (!menuArr) {
menuArr = [];
tabMap.set(request.uuid, 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.uuid);
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.uuid);
}
}
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

@ -1,535 +0,0 @@
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);
}
}
}

View File

@ -1,37 +0,0 @@
import { Script } from "@App/app/repo/scripts";
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 function getStorageName(script: Script): string {
if (script.metadata && script.metadata.storagename) {
return script.metadata.storagename[0];
}
return script.uuid;
}

View File

@ -0,0 +1,457 @@
// @copyright https://github.com/silverwzw/Tampermonkey-Typescript-Declaration
declare const unsafeWindow: Window;
declare type ConfigType = "text" | "checkbox" | "select" | "mult-select" | "number" | "textarea" | "time";
declare interface Config {
[key: string]: unknown;
title: string;
description: string;
default?: unknown;
type?: ConfigType;
bind?: string;
values?: unknown[];
password?: boolean;
// 文本类型时是字符串长度,数字类型时是最大值
max?: number;
min?: number;
rows?: number; // textarea行数
}
declare type UserConfig = { [key: string]: { [key: string]: Config } };
declare const GM_info: {
version: string;
scriptWillUpdate: boolean;
scriptHandler: "ScriptCat";
scriptUpdateURL?: string;
// scriptSource: string;
scriptMetaStr?: string;
userConfig?: UserConfig;
userConfigStr?: string;
// isIncognito: boolean;
// downloadMode: "native" | "disabled" | "browser";
script: {
author?: string;
description?: string;
// excludes: string[];
grant: string[];
header: string;
// homepage?: string;
icon?: string;
icon64?: string;
includes?: string[];
// lastModified: number;
matches: string[];
name: string;
namespace?: string;
// position: number;
"run-at": string;
// resources: string[];
// unwrap: boolean;
version: string;
/* options: {
awareOfChrome: boolean;
run_at: string;
noframes?: boolean;
compat_arrayLeft: boolean;
compat_foreach: boolean;
compat_forvarin: boolean;
compat_metadata: boolean;
compat_uW_gmonkey: boolean;
override: {
orig_excludes: string[];
orig_includes: string[];
use_includes: string[];
use_excludes: string[];
[key: string]: any;
};
[key: string]: any;
}; */
[key: string]: unknown;
};
[key: string]: unknown;
};
declare function GM_addStyle(css: string): HTMLElement;
declare function GM_deleteValue(name: string): void;
declare function GM_listValues(): string[];
declare function GM_addValueChangeListener(name: string, listener: GMTypes.ValueChangeListener): number;
declare function GM_removeValueChangeListener(listenerId: number): void;
// 可以使用Promise实际等待值的设置完成
declare function GM_setValue(name: string, value: unknown): Promise;
declare function GM_getValue(name: string, defaultValue?: unknown): unknown;
// 支持level和label
declare function GM_log(message: string, level?: GMTypes.LoggerLevel, labels?: GMTypes.LoggerLabel): unknown;
declare function GM_getResourceText(name: string): string | undefined;
declare function GM_getResourceURL(name: string, isBlobUrl?: boolean): string | undefined;
declare function GM_registerMenuCommand(name: string, listener: () => void, accessKey?: string): number;
declare function GM_unregisterMenuCommand(id: number): void;
declare function GM_openInTab(url: string, options: GMTypes.OpenTabOptions): tab;
declare function GM_openInTab(url: string, loadInBackground: boolean): tab;
declare function GM_openInTab(url: string): tab;
declare function GM_xmlhttpRequest(details: GMTypes.XHRDetails): GMTypes.AbortHandle<void>;
declare function GM_download(details: GMTypes.DownloadDetails): GMTypes.AbortHandle<boolean>;
declare function GM_download(url: string, filename: string): GMTypes.AbortHandle<boolean>;
declare function GM_getTab(callback: (obj: object) => unknown): void;
declare function GM_saveTab(obj: object): Promise<void>;
declare function GM_getTabs(callback: (objs: { [key: number]: object }) => unknown): void;
declare function GM_notification(details: GMTypes.NotificationDetails, ondone?: GMTypes.NotificationOnDone): void;
declare function GM_notification(
text: string,
title: string,
image: string,
onclick?: GMTypes.NotificationOnClick
): void;
declare function GM_closeNotification(id: string): void;
declare function GM_updateNotification(id: string, details: GMTypes.NotificationDetails): void;
declare function GM_setClipboard(data: string, info?: string | { type?: string; minetype?: string }): void;
declare function GM_addElement(tag: string, attribubutes: unknown);
declare function GM_addElement(parentNode: Element, tag: string, attrs: unknown);
// name和domain不能都为空
declare function GM_cookie(
action: GMTypes.CookieAction,
details: GMTypes.CookieDetails,
ondone: (cookie: GMTypes.Cookie[], error: unknown | undefined) => void
): void;
/**
* 可以通过GM_addValueChangeListener获取tabid
* 再通过tabid(前后端通信可能用到,ValueChangeListener会返回tabid),获取storeid,后台脚本用.
* 请注意这是一个实验性质的API,后续可能会改变
* @param tabid 页面的tabid
* @param ondone 完成事件
* @param callback.storeid 该页面的storeid,可以给GM_cookie使用
* @param callback.error 错误信息
* @deprecated 已废弃,请使用GM_cookie("store", tabid)替代
*/
declare function GM_getCookieStore(
tabid: number,
ondone: (storeId: number | undefined, error: unknown | undefined) => void
): void;
/**
* 设置浏览器代理
* @deprecated 正式版中已废弃,后续可能会在beta版本中添加
*/
declare function CAT_setProxy(rule: CATType.ProxyRule[] | string): void;
/**
* 清理所有代理规则
* @deprecated 正式版中已废弃,后续可能会在beta版本中添加
*/
declare function CAT_clearProxy(): void;
/**
* 输入x、y,模拟真实点击
* @deprecated 正式版中已废弃,后续可能会在beta版本中添加
*/
declare function CAT_click(x: number, y: number): void;
/**
* 打开脚本的用户配置页面
*/
declare function CAT_userConfig(): void;
/**
* 操控管理器设置的储存系统,将会在目录下创建一个app/uuid目录供此 API 使用,如果指定了baseDir参数,则会使用baseDir作为基础目录
* 上传时默认覆盖同名文件
* @param action 操作类型 list 列出指定目录所有文件, upload 上传文件, download 下载文件, delete 删除文件, config 打开配置页, 暂时不提供move/mkdir等操作
* @param details
*/
declare function CAT_fileStorage(
action: "list",
details: {
// 文件路径
path?: string;
// 基础目录,如果未设置,则将脚本uuid作为目录
baseDir?: string;
onload?: (files: CATType.FileStorageFileInfo[]) => void;
onerror?: (error: CATType.FileStorageError) => void;
}
): void;
declare function CAT_fileStorage(
action: "download",
details: {
file: CATType.FileStorageFileInfo; // 某些平台需要提供文件的hash值,所以需要传入文件信息
onload: (data: Blob) => void;
// onprogress?: (progress: number) => void;
onerror?: (error: CATType.FileStorageError) => void;
// public?: boolean;
}
): void;
declare function CAT_fileStorage(
action: "delete",
details: {
path: string;
onload?: () => void;
onerror?: (error: CATType.FileStorageError) => void;
// public?: boolean;
}
): void;
declare function CAT_fileStorage(
action: "upload",
details: {
path: string;
// 基础目录,如果未设置,则将脚本uuid作为目录
baseDir?: string;
data: Blob;
onload?: () => void;
// onprogress?: (progress: number) => void;
onerror?: (error: CATType.FileStorageError) => void;
// public?: boolean;
}
): void;
declare function CAT_fileStorage(action: "config"): void;
/**
* 脚本猫后台脚本重试, 当你的脚本出现错误时, 可以reject返回此错误, 以便脚本猫重试
* 重试时间请注意不要与脚本执行时间冲突, 否则可能会导致重复执行, 最小重试时间为5s
* @class CATRetryError
*/
declare class CATRetryError {
/**
* constructor 构造函数
* @param {string} message 错误信息
* @param {number} seconds x秒后重试, 单位秒
*/
constructor(message: string, seconds: number);
/**
* constructor 构造函数
* @param {string} message 错误信息
* @param {Date} date 重试时间, 指定时间后重试
*/
constructor(message: string, date: Date);
}
declare namespace CATType {
interface ProxyRule {
proxyServer: ProxyServer;
matchUrl: string[];
}
type ProxyScheme = "http" | "https" | "quic" | "socks4" | "socks5";
interface ProxyServer {
scheme?: ProxyScheme;
host: string;
port?: number;
}
interface FileStorageError {
// 错误码 -1 未知错误 1 用户未配置文件储存源 2 文件储存源配置错误 3 路径不存在
// 4 上传失败 5 下载失败 6 删除失败 7 不允许的文件路径 8 网络类型的错误
code: -1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
error: string;
}
interface FileStorageFileInfo {
// 文件名
name: string;
// 文件路径
path: string;
// 储存空间绝对路径
absPath: string;
// 文件大小
size: number;
// 文件摘要
digest: string;
// 文件创建时间
createtime: number;
// 文件修改时间
updatetime: number;
}
}
declare namespace GMTypes {
/*
* store为获取隐身窗口之类的cookie,这是一个实验性质的API,后续可能会改变
*/
type CookieAction = "list" | "delete" | "set" | "store";
type LoggerLevel = "debug" | "info" | "warn" | "error";
type LoggerLabel = {
[key: string]: string | boolean | number | undefined;
};
interface CookieDetails {
url?: string;
name?: string;
value?: string;
domain?: string;
path?: string;
secure?: boolean;
session?: boolean;
storeId?: string;
httpOnly?: boolean;
expirationDate?: number;
// store用
tabId?: number;
}
interface Cookie {
domain: string;
name: string;
storeId: string;
value: string;
session: boolean;
hostOnly: boolean;
expirationDate?: number;
path: string;
httpOnly: boolean;
secure: boolean;
}
// tabid是只有后台脚本监听才有的参数
type ValueChangeListener = (
name: string,
oldValue: unknown,
newValue: unknown,
remote: boolean,
tabid?: number
) => unknown;
interface OpenTabOptions {
active?: boolean;
insert?: boolean;
setParent?: boolean;
useOpen?: boolean; // 这是一个实验性/不兼容其他管理器/不兼容Firefox的功能
}
interface XHRResponse {
finalUrl?: string;
readyState?: 0 | 1 | 2 | 3 | 4;
responseHeaders?: string;
status?: number;
statusText?: string;
response?: string | Blob | ArrayBuffer | Document | ReadableStream | null;
responseText?: string;
responseXML?: Document | null;
responseType?: "text" | "arraybuffer" | "blob" | "json" | "document" | "stream";
}
interface XHRProgress extends XHRResponse {
done: number;
lengthComputable: boolean;
loaded: number;
position?: number;
total: number;
totalSize: number;
}
type Listener<OBJ> = (event: OBJ) => unknown;
type ContextType = unknown;
interface XHRDetails {
method?: "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
url: string;
headers?: { [key: string]: string };
data?: string | FormData | Blob;
cookie?: string;
binary?: boolean;
timeout?: number;
context?: ContextType;
responseType?: "text" | "arraybuffer" | "blob" | "json" | "document" | "stream"; // stream 在当前版本是一个较为简陋的实现
overrideMimeType?: string;
anonymous?: boolean;
fetch?: boolean;
user?: string;
password?: string;
nocache?: boolean;
maxRedirects?: number;
onload?: Listener<XHRResponse>;
onloadstart?: Listener<XHRResponse>;
onloadend?: Listener<XHRResponse>;
onprogress?: Listener<XHRProgress>;
onreadystatechange?: Listener<XHRResponse>;
ontimeout?: () => void;
onabort?: () => void;
onerror?: (err: string) => void;
}
interface AbortHandle<RETURN_TYPE> {
abort(): RETURN_TYPE;
}
interface DownloadError {
error: "not_enabled" | "not_whitelisted" | "not_permitted" | "not_supported" | "not_succeeded" | "unknown";
details?: string;
}
interface DownloadDetails {
method?: "GET" | "POST";
url: string;
name: string;
headers?: { [key: string]: string };
saveAs?: boolean;
timeout?: number;
cookie?: string;
anonymous?: boolean;
onerror?: Listener<DownloadError>;
ontimeout?: () => void;
onload?: Listener<object>;
onprogress?: Listener<XHRProgress>;
}
interface NotificationThis extends NotificationDetails {
id: string;
}
type NotificationOnClick = (this: NotificationThis, id: string, index?: number) => unknown;
type NotificationOnDone = (this: NotificationThis, user: boolean) => unknown;
interface NotificationButton {
title: string;
iconUrl?: string;
}
interface NotificationDetails {
text?: string;
title?: string;
image?: string;
highlight?: boolean;
silent?: boolean;
timeout?: number;
onclick?: NotificationOnClick;
ondone?: NotificationOnDone;
progress?: number;
oncreate?: NotificationOnClick;
buttons?: NotificationButton[];
}
interface Tab {
close(): void;
onclose?: () => void;
closed?: boolean;
name?: string;
}
}