处理后台脚本API
This commit is contained in:
parent
239f961485
commit
a2870eb18e
@ -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());
|
|
17
example/gm_value/gm_value_1_bg.js
Normal file
17
example/gm_value/gm_value_1_bg.js
Normal 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();
|
||||||
|
});
|
@ -23,6 +23,6 @@ GM_addValueChangeListener("test_set", function (name, oldval, newval, remote, ta
|
|||||||
});
|
});
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
console.log(GM_getValue("test_set"));
|
console.log("test_set: ", GM_getValue("test_set"));
|
||||||
console.log(GM_listValues());
|
console.log("value list:", GM_listValues());
|
||||||
}, 2000);
|
}, 2000);
|
32
example/gm_value/gm_value_2_bg.js
Normal file
32
example/gm_value/gm_value_2_bg.js
Normal 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()
|
||||||
|
});
|
@ -72,8 +72,8 @@ export class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private messageHandle(msg: string, params: any, sendResponse: (response: any) => void, sender?: MessageSender) {
|
private messageHandle(action: string, params: any, sendResponse: (response: any) => void, sender?: MessageSender) {
|
||||||
const func = this.apiFunctionMap.get(msg);
|
const func = this.apiFunctionMap.get(action);
|
||||||
if (func) {
|
if (func) {
|
||||||
try {
|
try {
|
||||||
const ret = func(params, new GetSender(sender!));
|
const ret = func(params, new GetSender(sender!));
|
||||||
@ -89,8 +89,8 @@ export class Server {
|
|||||||
sendResponse({ code: -1, message: e.message });
|
sendResponse({ code: -1, message: e.message });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sendResponse({ code: -1, message: "no such api" });
|
sendResponse({ code: -1, message: "no such api " + action });
|
||||||
this.logger.error("no such api", { msg });
|
this.logger.error("no such api", { action: action });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,16 +135,39 @@ export default class Cache {
|
|||||||
return this.storage.list();
|
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> {
|
public async tx<T>(key: string, set: (result: T) => Promise<T>): Promise<T> {
|
||||||
let promise = this.txPromise.get(key);
|
const unlock = await this.lock(key);
|
||||||
if (promise) {
|
|
||||||
await promise;
|
|
||||||
}
|
|
||||||
let newValue: T;
|
let newValue: T;
|
||||||
promise = this.get(key)
|
await this.get(key)
|
||||||
.then((result) => set(result))
|
.then((result) => set(result))
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
@ -153,9 +176,7 @@ export default class Cache {
|
|||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
this.txPromise.set(key, promise);
|
unlock();
|
||||||
await promise;
|
|
||||||
this.txPromise.delete(key);
|
|
||||||
return newValue!;
|
return newValue!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,9 +12,9 @@ export default class ContentRuntime {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
start(scripts: ScriptRunResouce[]) {
|
start(scripts: ScriptRunResouce[]) {
|
||||||
this.extServer.on("runtime/menuClick", (data) => {
|
this.extServer.on("runtime/emitEvent", (data) => {
|
||||||
// 转发给inject
|
// 转发给inject
|
||||||
return sendMessage(this.msg, "inject/runtime/menuClick", data);
|
return sendMessage(this.msg, "inject/runtime/emitEvent", data);
|
||||||
});
|
});
|
||||||
this.extServer.on("runtime/valueUpdate", (data) => {
|
this.extServer.on("runtime/valueUpdate", (data) => {
|
||||||
// 转发给inject
|
// 转发给inject
|
@ -69,13 +69,16 @@ export default class ExecScript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发值更新
|
emitEvent(event: string, data: any) {
|
||||||
valueUpdate(data: ValueUpdateData) {
|
switch (event) {
|
||||||
this.sandboxContent?.valueUpdate(data);
|
case "menuClick":
|
||||||
|
this.sandboxContent?.menuClick(data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
menuClick(id: number) {
|
valueUpdate(data: ValueUpdateData) {
|
||||||
this.sandboxContent?.menuClick(id);
|
this.sandboxContent?.valueUpdate(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
exec() {
|
exec() {
|
@ -2,12 +2,12 @@ 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 { ValueUpdateData } from "./exec_script";
|
import { ValueUpdateData } from "./exec_script";
|
||||||
import { ExtVersion } from "@App/app/const";
|
import { ExtVersion } from "@App/app/const";
|
||||||
import { getStorageName } from "../utils";
|
|
||||||
import { Message, MessageConnect } from "@Packages/message/server";
|
import { Message, MessageConnect } from "@Packages/message/server";
|
||||||
import { CustomEventMessage } from "@Packages/message/custom_event_message";
|
import { CustomEventMessage } from "@Packages/message/custom_event_message";
|
||||||
import LoggerCore from "@App/app/logger/core";
|
import LoggerCore from "@App/app/logger/core";
|
||||||
import { connect, sendMessage } from "@Packages/message/client";
|
import { connect, sendMessage } from "@Packages/message/client";
|
||||||
import EventEmitter from "eventemitter3";
|
import EventEmitter from "eventemitter3";
|
||||||
|
import { getStorageName } from "@App/pkg/utils/utils";
|
||||||
|
|
||||||
interface ApiParam {
|
interface ApiParam {
|
||||||
depend?: string[];
|
depend?: string[];
|
||||||
@ -174,17 +174,17 @@ export default class GMApi {
|
|||||||
this.GM_setValue(name, undefined);
|
this.GM_setValue(name, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
valueChangeId: number | undefined;
|
eventId: number = 0;
|
||||||
|
|
||||||
|
menuMap: Map<number, string> | undefined;
|
||||||
|
|
||||||
|
EE: EventEmitter = new EventEmitter();
|
||||||
|
|
||||||
@GMContext.API()
|
@GMContext.API()
|
||||||
public GM_addValueChangeListener(name: string, listener: GMTypes.ValueChangeListener): number {
|
public GM_addValueChangeListener(name: string, listener: GMTypes.ValueChangeListener): number {
|
||||||
if (!this.valueChangeId) {
|
this.eventId += 1;
|
||||||
this.valueChangeId = 1;
|
this.valueChangeListener.set(this.eventId, { name, listener });
|
||||||
} else {
|
return this.eventId;
|
||||||
this.valueChangeId += 1;
|
|
||||||
}
|
|
||||||
this.valueChangeListener.set(this.valueChangeId, { name, listener });
|
|
||||||
return this.valueChangeId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GMContext.API()
|
@GMContext.API()
|
||||||
@ -222,12 +222,6 @@ export default class GMApi {
|
|||||||
return (<CustomEventMessage>this.message).getAndDelRelatedTarget(data.relatedTarget);
|
return (<CustomEventMessage>this.message).getAndDelRelatedTarget(data.relatedTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
menuId: number | undefined;
|
|
||||||
|
|
||||||
menuMap: Map<number, string> | undefined;
|
|
||||||
|
|
||||||
EE: EventEmitter = new EventEmitter();
|
|
||||||
|
|
||||||
@GMContext.API()
|
@GMContext.API()
|
||||||
GM_registerMenuCommand(name: string, listener: () => void, accessKey?: string): number {
|
GM_registerMenuCommand(name: string, listener: () => void, accessKey?: string): number {
|
||||||
if (!this.menuMap) {
|
if (!this.menuMap) {
|
||||||
@ -240,15 +234,10 @@ export default class GMApi {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (flag) {
|
if (flag) {
|
||||||
this.EE.addListener("menuClick" + flag, listener);
|
|
||||||
return flag;
|
return flag;
|
||||||
}
|
}
|
||||||
if (!this.menuId) {
|
this.eventId += 1;
|
||||||
this.menuId = 1;
|
const id = this.eventId;
|
||||||
} else {
|
|
||||||
this.menuId += 1;
|
|
||||||
}
|
|
||||||
const id = this.menuId;
|
|
||||||
this.menuMap.set(id, name);
|
this.menuMap.set(id, name);
|
||||||
this.EE.addListener("menuClick" + id, listener);
|
this.EE.addListener("menuClick" + id, listener);
|
||||||
this.sendMessage("GM_registerMenuCommand", [id, name, accessKey]);
|
this.sendMessage("GM_registerMenuCommand", [id, name, accessKey]);
|
||||||
@ -257,6 +246,7 @@ export default class GMApi {
|
|||||||
|
|
||||||
@GMContext.API()
|
@GMContext.API()
|
||||||
GM_unregisterMenuCommand(id: number): void {
|
GM_unregisterMenuCommand(id: number): void {
|
||||||
|
console.log("unregisterMenuCommand", id);
|
||||||
if (!this.menuMap) {
|
if (!this.menuMap) {
|
||||||
this.menuMap = new Map();
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,7 +2,8 @@ import { ScriptRunResouce } from "@App/app/repo/scripts";
|
|||||||
import { Message, Server } from "@Packages/message/server";
|
import { Message, Server } from "@Packages/message/server";
|
||||||
import ExecScript, { ValueUpdateData } from "./exec_script";
|
import ExecScript, { ValueUpdateData } from "./exec_script";
|
||||||
import { addStyle, ScriptFunc } from "./utils";
|
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 {
|
export class InjectRuntime {
|
||||||
execList: ExecScript[] = [];
|
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);
|
const exec = this.execList.find((val) => val.scriptRes.uuid === data.uuid);
|
||||||
if (exec) {
|
if (exec) {
|
||||||
exec.menuClick(data.id);
|
exec.emitEvent(data.event, data.data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.server.on("runtime/valueUpdate", (data: ValueUpdateData) => {
|
this.server.on("runtime/valueUpdate", (data: ValueUpdateData) => {
|
@ -64,6 +64,7 @@ export function createContext(scriptRes: ScriptRunResouce, GMInfo: any, envPrefi
|
|||||||
sendMessage: GMApi.prototype.sendMessage,
|
sendMessage: GMApi.prototype.sendMessage,
|
||||||
connect: GMApi.prototype.connect,
|
connect: GMApi.prototype.connect,
|
||||||
runFlag: uuidv4(),
|
runFlag: uuidv4(),
|
||||||
|
eventId: 10000,
|
||||||
valueUpdate: GMApi.prototype.valueUpdate,
|
valueUpdate: GMApi.prototype.valueUpdate,
|
||||||
menuClick: GMApi.prototype.menuClick,
|
menuClick: GMApi.prototype.menuClick,
|
||||||
EE: new EventEmitter(),
|
EE: new EventEmitter(),
|
@ -48,7 +48,9 @@ export class OffscreenManager {
|
|||||||
script.init();
|
script.init();
|
||||||
// 转发从sandbox来的gm api请求
|
// 转发从sandbox来的gm api请求
|
||||||
forwardMessage("serviceWorker", "runtime/gmApi", this.windowServer, this.extensionMessage);
|
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"));
|
const gmApi = new GMApi(this.windowServer.group("gmApi"));
|
||||||
gmApi.init();
|
gmApi.init();
|
||||||
|
@ -7,12 +7,14 @@ import {
|
|||||||
SCRIPT_TYPE_BACKGROUND,
|
SCRIPT_TYPE_BACKGROUND,
|
||||||
ScriptRunResouce,
|
ScriptRunResouce,
|
||||||
} from "@App/app/repo/scripts";
|
} 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 { Server } from "@Packages/message/server";
|
||||||
import { WindowMessage } from "@Packages/message/window_message";
|
import { WindowMessage } from "@Packages/message/window_message";
|
||||||
import { CronJob } from "cron";
|
import { CronJob } from "cron";
|
||||||
import { proxyUpdateRunStatus } from "../offscreen/client";
|
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 {
|
export class Runtime {
|
||||||
cronJob: Map<string, Array<CronJob>> = new Map();
|
cronJob: Map<string, Array<CronJob>> = new Map();
|
||||||
@ -290,10 +292,30 @@ export class Runtime {
|
|||||||
return this.execScript(script, true);
|
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() {
|
init() {
|
||||||
this.api.on("enableScript", this.enableScript.bind(this));
|
this.api.on("enableScript", this.enableScript.bind(this));
|
||||||
this.api.on("disableScript", this.disableScript.bind(this));
|
this.api.on("disableScript", this.disableScript.bind(this));
|
||||||
this.api.on("runScript", this.runScript.bind(this));
|
this.api.on("runScript", this.runScript.bind(this));
|
||||||
this.api.on("stopScript", this.stopScript.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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import { ValueService } from "@App/app/service/service_worker/value";
|
|||||||
import PermissionVerify from "./permission_verify";
|
import PermissionVerify from "./permission_verify";
|
||||||
import { connect } from "@Packages/message/client";
|
import { connect } from "@Packages/message/client";
|
||||||
import Cache, { incr } from "@App/app/cache";
|
import Cache, { incr } from "@App/app/cache";
|
||||||
import { unsafeHeaders } from "@App/runtime/utils";
|
|
||||||
import EventEmitter from "eventemitter3";
|
import EventEmitter from "eventemitter3";
|
||||||
import { MessageQueue } from "@Packages/message/message_queue";
|
import { MessageQueue } from "@Packages/message/message_queue";
|
||||||
import { RuntimeService } from "./runtime";
|
import { RuntimeService } from "./runtime";
|
||||||
@ -24,6 +23,35 @@ export type Request = MessageRequest & {
|
|||||||
script: Script;
|
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 type Api = (request: Request, con: GetSender) => Promise<any>;
|
||||||
|
|
||||||
export default class GMApi {
|
export default class GMApi {
|
||||||
@ -194,7 +222,7 @@ export default class GMApi {
|
|||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
accessKey: accessKey,
|
accessKey: accessKey,
|
||||||
tabId: sender.getSender().tab!.id!,
|
tabId: sender.getSender().tab?.id || -1,
|
||||||
frameId: sender.getSender().frameId,
|
frameId: sender.getSender().frameId,
|
||||||
documentId: sender.getSender().documentId,
|
documentId: sender.getSender().documentId,
|
||||||
});
|
});
|
||||||
@ -207,7 +235,7 @@ export default class GMApi {
|
|||||||
this.mq.emit("unregisterMenuCommand", {
|
this.mq.emit("unregisterMenuCommand", {
|
||||||
uuid: request.script.uuid,
|
uuid: request.script.uuid,
|
||||||
id: id,
|
id: id,
|
||||||
tabId: sender.getSender().tab!.id!,
|
tabId: sender.getSender().tab?.id || -1,
|
||||||
frameId: sender.getSender().frameId,
|
frameId: sender.getSender().frameId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
subscribeScriptMenuRegister,
|
subscribeScriptMenuRegister,
|
||||||
subscribeScriptRunStatus,
|
subscribeScriptRunStatus,
|
||||||
} from "../queue";
|
} from "../queue";
|
||||||
import { getStorageName } from "@App/runtime/utils";
|
import { getStorageName } from "@App/pkg/utils/utils";
|
||||||
|
|
||||||
export type ScriptMenuItem = {
|
export type ScriptMenuItem = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -206,7 +206,7 @@ export class PopupService {
|
|||||||
|
|
||||||
// 事务更新脚本菜单
|
// 事务更新脚本菜单
|
||||||
txUpdateScriptMenu(tabId: number, callback: (menu: ScriptMenu[]) => Promise<any>) {
|
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 || []);
|
return callback(menu || []);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -252,14 +252,15 @@ export class PopupService {
|
|||||||
if (script.type === SCRIPT_TYPE_NORMAL) {
|
if (script.type === SCRIPT_TYPE_NORMAL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (script.status !== SCRIPT_STATUS_ENABLE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return this.txUpdateScriptMenu(-1, async (menu) => {
|
return this.txUpdateScriptMenu(-1, async (menu) => {
|
||||||
const scriptMenu = menu.find((item) => item.uuid === script.uuid);
|
const scriptMenu = menu.find((item) => item.uuid === script.uuid);
|
||||||
if (script.status === SCRIPT_STATUS_ENABLE) {
|
// 加入菜单
|
||||||
// 加入菜单
|
if (!scriptMenu) {
|
||||||
if (!scriptMenu) {
|
const item = this.scriptToMenu(script);
|
||||||
const item = this.scriptToMenu(script);
|
menu.push(item);
|
||||||
menu.push(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return menu;
|
return menu;
|
||||||
});
|
});
|
||||||
@ -294,24 +295,22 @@ export class PopupService {
|
|||||||
const index = menu.findIndex((item) => item.uuid === uuid);
|
const index = menu.findIndex((item) => item.uuid === uuid);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
menu.splice(index, 1);
|
menu.splice(index, 1);
|
||||||
return menu;
|
|
||||||
}
|
}
|
||||||
return null;
|
return menu;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
subscribeScriptRunStatus(this.mq, async ({ uuid, runStatus }) => {
|
subscribeScriptRunStatus(this.mq, async ({ uuid, runStatus }) => {
|
||||||
return this.txUpdateScriptMenu(-1, async (menu) => {
|
return this.txUpdateScriptMenu(-1, async (menu) => {
|
||||||
const scriptMenu = menu.find((item) => item.uuid === uuid);
|
const scriptMenu = menu.find((item) => item.uuid === uuid);
|
||||||
if (scriptMenu) {
|
if (scriptMenu) {
|
||||||
if (scriptMenu.runStatus === SCRIPT_RUN_STATUS_RUNNING) {
|
scriptMenu.runStatus = runStatus;
|
||||||
|
if (runStatus === SCRIPT_RUN_STATUS_RUNNING) {
|
||||||
scriptMenu.runNum = 1;
|
scriptMenu.runNum = 1;
|
||||||
} else {
|
} else {
|
||||||
scriptMenu.runNum = 0;
|
scriptMenu.runNum = 0;
|
||||||
}
|
}
|
||||||
scriptMenu.runStatus = runStatus;
|
|
||||||
return menu;
|
|
||||||
}
|
}
|
||||||
return null;
|
return menu;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -330,13 +329,12 @@ export class PopupService {
|
|||||||
documentId: string;
|
documentId: string;
|
||||||
}) {
|
}) {
|
||||||
// 菜单点击事件
|
// 菜单点击事件
|
||||||
this.runtime.sendMessageToTab(
|
this.runtime.EmitEventToTab(
|
||||||
tabId,
|
tabId,
|
||||||
"menuClick",
|
|
||||||
{
|
{
|
||||||
uuid,
|
uuid,
|
||||||
id,
|
event: "menuClick",
|
||||||
tabId,
|
data: id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
frameId,
|
frameId,
|
||||||
@ -372,14 +370,21 @@ export class PopupService {
|
|||||||
const [, , uuid, id] = menuIds;
|
const [, , uuid, id] = menuIds;
|
||||||
// 寻找menu信息
|
// 寻找menu信息
|
||||||
const menu = await this.getScriptMenu(tab!.id!);
|
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) {
|
if (script) {
|
||||||
const menuItem = script.menus.find((item) => item.id === parseInt(id, 10));
|
const menuItem = script.menus.find((item) => item.id === parseInt(id, 10));
|
||||||
if (menuItem) {
|
if (menuItem) {
|
||||||
this.menuClick({
|
this.menuClick({
|
||||||
uuid: script.uuid,
|
uuid: script.uuid,
|
||||||
id: menuItem.id,
|
id: menuItem.id,
|
||||||
tabId: tab!.id!,
|
tabId: bgscript ? -1 : tab!.id!,
|
||||||
frameId: menuItem.frameId || 0,
|
frameId: menuItem.frameId || 0,
|
||||||
documentId: menuItem.documentId || "",
|
documentId: menuItem.documentId || "",
|
||||||
});
|
});
|
||||||
|
@ -16,11 +16,11 @@ import { ScriptService } from "./script";
|
|||||||
import { runScript, stopScript } from "../offscreen/client";
|
import { runScript, stopScript } from "../offscreen/client";
|
||||||
import { getRunAt } from "./utils";
|
import { getRunAt } from "./utils";
|
||||||
import { randomString } from "@App/pkg/utils/utils";
|
import { randomString } from "@App/pkg/utils/utils";
|
||||||
import { compileInjectScript } from "@App/runtime/content/utils";
|
|
||||||
import Cache from "@App/app/cache";
|
import Cache from "@App/app/cache";
|
||||||
import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match";
|
import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match";
|
||||||
import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
|
import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
|
||||||
import { sendMessage } from "@Packages/message/client";
|
import { sendMessage } from "@Packages/message/client";
|
||||||
|
import { compileInjectScript } from "../content/utils";
|
||||||
|
|
||||||
// 为了优化性能,存储到缓存时删除了code与value
|
// 为了优化性能,存储到缓存时删除了code与value
|
||||||
export interface ScriptMatchInfo extends ScriptRunResouce {
|
export interface ScriptMatchInfo extends ScriptRunResouce {
|
||||||
@ -29,6 +29,12 @@ export interface ScriptMatchInfo extends ScriptRunResouce {
|
|||||||
customizeExcludeMatches: string[];
|
customizeExcludeMatches: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EmitEventRequest {
|
||||||
|
uuid: string;
|
||||||
|
event: string;
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
export class RuntimeService {
|
export class RuntimeService {
|
||||||
scriptDAO: ScriptDAO = new ScriptDAO();
|
scriptDAO: ScriptDAO = new ScriptDAO();
|
||||||
|
|
||||||
@ -132,6 +138,22 @@ export class RuntimeService {
|
|||||||
return sendMessage(new ExtensionContentMessageSend(tabId, options), "content/runtime/" + action, data);
|
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) {
|
async getPageScriptUuidByUrl(url: string, includeCustomize?: boolean) {
|
||||||
const match = await this.loadScriptMatchInfo();
|
const match = await this.loadScriptMatchInfo();
|
||||||
// 匹配当前页面的脚本
|
// 匹配当前页面的脚本
|
||||||
|
@ -19,7 +19,7 @@ import { MessageQueue } from "@Packages/message/message_queue";
|
|||||||
import { InstallSource } from ".";
|
import { InstallSource } from ".";
|
||||||
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 "../content/utils";
|
||||||
|
|
||||||
export class ScriptService {
|
export class ScriptService {
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
@ -2,13 +2,13 @@ import LoggerCore from "@App/app/logger/core";
|
|||||||
import Logger from "@App/app/logger/logger";
|
import Logger from "@App/app/logger/logger";
|
||||||
import { Script, SCRIPT_TYPE_NORMAL, ScriptDAO } from "@App/app/repo/scripts";
|
import { Script, SCRIPT_TYPE_NORMAL, ScriptDAO } from "@App/app/repo/scripts";
|
||||||
import { ValueDAO } from "@App/app/repo/value";
|
import { ValueDAO } from "@App/app/repo/value";
|
||||||
import { getStorageName } from "@App/runtime/utils";
|
|
||||||
import { Group, MessageSend } from "@Packages/message/server";
|
import { Group, MessageSend } from "@Packages/message/server";
|
||||||
import { RuntimeService } from "./runtime";
|
import { RuntimeService } from "./runtime";
|
||||||
import { PopupService } from "./popup";
|
import { PopupService } from "./popup";
|
||||||
import { ValueUpdateData, ValueUpdateSender } from "@App/runtime/content/exec_script";
|
|
||||||
import { sendMessage } from "@Packages/message/client";
|
import { sendMessage } from "@Packages/message/client";
|
||||||
import Cache from "@App/app/cache";
|
import Cache from "@App/app/cache";
|
||||||
|
import { getStorageName } from "@App/pkg/utils/utils";
|
||||||
|
import { ValueUpdateData, ValueUpdateSender } from "../content/exec_script";
|
||||||
|
|
||||||
export class ValueService {
|
export class ValueService {
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
@ -67,21 +67,18 @@ export class ValueService {
|
|||||||
uuid,
|
uuid,
|
||||||
storageName: storageName,
|
storageName: storageName,
|
||||||
};
|
};
|
||||||
// 判断是后台脚本还是前台脚本
|
|
||||||
if (script.type === SCRIPT_TYPE_NORMAL) {
|
chrome.tabs.query({}, (tabs) => {
|
||||||
chrome.tabs.query({}, (tabs) => {
|
// 推送到所有加载了本脚本的tab中
|
||||||
// 推送到所有加载了本脚本的tab中
|
tabs.forEach(async (tab) => {
|
||||||
tabs.forEach(async (tab) => {
|
const scriptMenu = await this.popup!.getScriptMenu(tab.id!);
|
||||||
const scriptMenu = await this.popup!.getScriptMenu(tab.id!);
|
if (scriptMenu.find((item) => item.storageName === storageName)) {
|
||||||
if (scriptMenu.find((item) => item.storageName === storageName)) {
|
this.runtime!.sendMessageToTab(tab.id!, "valueUpdate", sendData);
|
||||||
this.runtime!.sendMessageToTab(tab.id!, "valueUpdate", sendData);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else {
|
});
|
||||||
// 推送到offscreen中
|
// 推送到offscreen中
|
||||||
sendMessage(this.send, "offscreen/runtime/valueUpdate", sendData);
|
sendMessage(this.send, "offscreen/runtime/valueUpdate", sendData);
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ import MessageWriter from "./app/logger/message_writer";
|
|||||||
import { ExtensionMessage, ExtensionMessageSend } from "@Packages/message/extension_message";
|
import { ExtensionMessage, ExtensionMessageSend } from "@Packages/message/extension_message";
|
||||||
import { CustomEventMessage } from "@Packages/message/custom_event_message";
|
import { CustomEventMessage } from "@Packages/message/custom_event_message";
|
||||||
import { RuntimeClient } from "./app/service/service_worker/client";
|
import { RuntimeClient } from "./app/service/service_worker/client";
|
||||||
import ContentRuntime from "./runtime/content/content";
|
|
||||||
import { Server } from "@Packages/message/server";
|
import { Server } from "@Packages/message/server";
|
||||||
|
import ContentRuntime from "./app/service/content/content";
|
||||||
|
|
||||||
// 建立与service_worker页面的连接
|
// 建立与service_worker页面的连接
|
||||||
const send = new ExtensionMessageSend();
|
const send = new ExtensionMessageSend();
|
||||||
|
@ -2,8 +2,8 @@ import LoggerCore from "./app/logger/core";
|
|||||||
import MessageWriter from "./app/logger/message_writer";
|
import MessageWriter from "./app/logger/message_writer";
|
||||||
import { CustomEventMessage } from "@Packages/message/custom_event_message";
|
import { CustomEventMessage } from "@Packages/message/custom_event_message";
|
||||||
import { Server } from "@Packages/message/server";
|
import { Server } from "@Packages/message/server";
|
||||||
import { InjectRuntime } from "./runtime/content/inject";
|
|
||||||
import { ScriptRunResouce } from "./app/repo/scripts";
|
import { ScriptRunResouce } from "./app/repo/scripts";
|
||||||
|
import { InjectRuntime } from "./app/service/content/inject";
|
||||||
|
|
||||||
const msg = new CustomEventMessage(MessageFlag, false);
|
const msg = new CustomEventMessage(MessageFlag, false);
|
||||||
|
|
||||||
|
@ -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";
|
import { languages } from "monaco-editor";
|
||||||
|
|
||||||
// 注册eslint
|
// 注册eslint
|
||||||
// const linterWorker = new Worker("/src/linter.worker.js");
|
// const linterWorker = new Worker("/src/linter.worker.js");
|
||||||
|
|
||||||
|
console.log(dts, dts.length);
|
||||||
|
|
||||||
export default function registerEditor() {
|
export default function registerEditor() {
|
||||||
window.MonacoEnvironment = {
|
window.MonacoEnvironment = {
|
||||||
getWorkerUrl(moduleId: any, label: any) {
|
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 } = {
|
const prompt: { [key: string]: any } = {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Metadata } from "@App/app/repo/scripts";
|
import { Metadata, Script } from "@App/app/repo/scripts";
|
||||||
import { CronTime } from "cron";
|
import { CronTime } from "cron";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
@ -217,3 +217,10 @@ export function sleep(time: number) {
|
|||||||
setTimeout(resolve, time);
|
setTimeout(resolve, time);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStorageName(script: Script): string {
|
||||||
|
if (script.metadata && script.metadata.storagename) {
|
||||||
|
return script.metadata.storagename[0];
|
||||||
|
}
|
||||||
|
return script.uuid;
|
||||||
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
457
src/template/scriptcat.d.tpl
Normal file
457
src/template/scriptcat.d.tpl
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user