diff --git a/example/gm_value.js b/example/gm_value.js deleted file mode 100644 index 4ad13cf..0000000 --- a/example/gm_value.js +++ /dev/null @@ -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()); diff --git a/example/sotrage_name/gm_value_1.js b/example/gm_value/gm_value_1.js similarity index 100% rename from example/sotrage_name/gm_value_1.js rename to example/gm_value/gm_value_1.js diff --git a/example/gm_value/gm_value_1_bg.js b/example/gm_value/gm_value_1_bg.js new file mode 100644 index 0000000..5978aa8 --- /dev/null +++ b/example/gm_value/gm_value_1_bg.js @@ -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(); +}); diff --git a/example/sotrage_name/gm_value_2.js b/example/gm_value/gm_value_2.js similarity index 89% rename from example/sotrage_name/gm_value_2.js rename to example/gm_value/gm_value_2.js index d4cf8b5..4baa2a0 100644 --- a/example/sotrage_name/gm_value_2.js +++ b/example/gm_value/gm_value_2.js @@ -23,6 +23,6 @@ GM_addValueChangeListener("test_set", function (name, oldval, newval, remote, ta }); setInterval(() => { - console.log(GM_getValue("test_set")); - console.log(GM_listValues()); + console.log("test_set: ", GM_getValue("test_set")); + console.log("value list:", GM_listValues()); }, 2000); diff --git a/example/gm_value/gm_value_2_bg.js b/example/gm_value/gm_value_2_bg.js new file mode 100644 index 0000000..fb6750b --- /dev/null +++ b/example/gm_value/gm_value_2_bg.js @@ -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() +}); diff --git a/packages/message/server.ts b/packages/message/server.ts index 0fc1eab..7fc61bc 100644 --- a/packages/message/server.ts +++ b/packages/message/server.ts @@ -72,8 +72,8 @@ export class Server { } } - private messageHandle(msg: string, params: any, sendResponse: (response: any) => void, sender?: MessageSender) { - const func = this.apiFunctionMap.get(msg); + private messageHandle(action: string, params: any, sendResponse: (response: any) => void, sender?: MessageSender) { + const func = this.apiFunctionMap.get(action); if (func) { try { const ret = func(params, new GetSender(sender!)); @@ -89,8 +89,8 @@ export class Server { sendResponse({ code: -1, message: e.message }); } } else { - sendResponse({ code: -1, message: "no such api" }); - this.logger.error("no such api", { msg }); + sendResponse({ code: -1, message: "no such api " + action }); + this.logger.error("no such api", { action: action }); } } } diff --git a/src/app/cache.ts b/src/app/cache.ts index 0222344..4c74986 100644 --- a/src/app/cache.ts +++ b/src/app/cache.ts @@ -135,16 +135,39 @@ export default class Cache { return this.storage.list(); } - private txPromise: Map> = new Map(); + private txLock: Map 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(key: string, set: (result: T) => Promise): Promise { - let promise = this.txPromise.get(key); - if (promise) { - await promise; - } + const unlock = await this.lock(key); let newValue: T; - promise = this.get(key) + await this.get(key) .then((result) => set(result)) .then((value) => { if (value) { @@ -153,9 +176,7 @@ export default class Cache { } return Promise.resolve(); }); - this.txPromise.set(key, promise); - await promise; - this.txPromise.delete(key); + unlock(); return newValue!; } } diff --git a/src/runtime/content/content.ts b/src/app/service/content/content.ts similarity index 93% rename from src/runtime/content/content.ts rename to src/app/service/content/content.ts index 9e0c4c6..9588762 100644 --- a/src/runtime/content/content.ts +++ b/src/app/service/content/content.ts @@ -12,9 +12,9 @@ export default class ContentRuntime { ) {} start(scripts: ScriptRunResouce[]) { - this.extServer.on("runtime/menuClick", (data) => { + this.extServer.on("runtime/emitEvent", (data) => { // 转发给inject - return sendMessage(this.msg, "inject/runtime/menuClick", data); + return sendMessage(this.msg, "inject/runtime/emitEvent", data); }); this.extServer.on("runtime/valueUpdate", (data) => { // 转发给inject diff --git a/src/runtime/content/exec_script.test.ts b/src/app/service/content/exec_script.test.ts similarity index 100% rename from src/runtime/content/exec_script.test.ts rename to src/app/service/content/exec_script.test.ts diff --git a/src/runtime/content/exec_script.ts b/src/app/service/content/exec_script.ts similarity index 93% rename from src/runtime/content/exec_script.ts rename to src/app/service/content/exec_script.ts index 0e9e793..f135441 100644 --- a/src/runtime/content/exec_script.ts +++ b/src/app/service/content/exec_script.ts @@ -69,13 +69,16 @@ export default class ExecScript { } } - // 触发值更新 - valueUpdate(data: ValueUpdateData) { - this.sandboxContent?.valueUpdate(data); + emitEvent(event: string, data: any) { + switch (event) { + case "menuClick": + this.sandboxContent?.menuClick(data); + break; + } } - menuClick(id: number) { - this.sandboxContent?.menuClick(id); + valueUpdate(data: ValueUpdateData) { + this.sandboxContent?.valueUpdate(data); } exec() { diff --git a/src/runtime/content/exec_warp.ts b/src/app/service/content/exec_warp.ts similarity index 100% rename from src/runtime/content/exec_warp.ts rename to src/app/service/content/exec_warp.ts diff --git a/src/runtime/content/gm_api.ts b/src/app/service/content/gm_api.ts similarity index 87% rename from src/runtime/content/gm_api.ts rename to src/app/service/content/gm_api.ts index e2c3f00..4ac4f1f 100644 --- a/src/runtime/content/gm_api.ts +++ b/src/app/service/content/gm_api.ts @@ -2,12 +2,12 @@ import { ScriptRunResouce } from "@App/app/repo/scripts"; import { getMetadataStr, getUserConfigStr, parseUserConfig } from "@App/pkg/utils/script"; import { ValueUpdateData } from "./exec_script"; import { ExtVersion } from "@App/app/const"; -import { getStorageName } from "../utils"; import { Message, MessageConnect } from "@Packages/message/server"; import { CustomEventMessage } from "@Packages/message/custom_event_message"; import LoggerCore from "@App/app/logger/core"; import { connect, sendMessage } from "@Packages/message/client"; import EventEmitter from "eventemitter3"; +import { getStorageName } from "@App/pkg/utils/utils"; interface ApiParam { depend?: string[]; @@ -174,17 +174,17 @@ export default class GMApi { this.GM_setValue(name, undefined); } - valueChangeId: number | undefined; + eventId: number = 0; + + menuMap: Map | undefined; + + EE: EventEmitter = new EventEmitter(); @GMContext.API() public GM_addValueChangeListener(name: string, listener: GMTypes.ValueChangeListener): number { - if (!this.valueChangeId) { - this.valueChangeId = 1; - } else { - this.valueChangeId += 1; - } - this.valueChangeListener.set(this.valueChangeId, { name, listener }); - return this.valueChangeId; + this.eventId += 1; + this.valueChangeListener.set(this.eventId, { name, listener }); + return this.eventId; } @GMContext.API() @@ -222,12 +222,6 @@ export default class GMApi { return (this.message).getAndDelRelatedTarget(data.relatedTarget); } - menuId: number | undefined; - - menuMap: Map | undefined; - - EE: EventEmitter = new EventEmitter(); - @GMContext.API() GM_registerMenuCommand(name: string, listener: () => void, accessKey?: string): number { if (!this.menuMap) { @@ -240,15 +234,10 @@ export default class GMApi { } }); if (flag) { - this.EE.addListener("menuClick" + flag, listener); return flag; } - if (!this.menuId) { - this.menuId = 1; - } else { - this.menuId += 1; - } - const id = this.menuId; + this.eventId += 1; + const id = this.eventId; this.menuMap.set(id, name); this.EE.addListener("menuClick" + id, listener); this.sendMessage("GM_registerMenuCommand", [id, name, accessKey]); @@ -257,6 +246,7 @@ export default class GMApi { @GMContext.API() GM_unregisterMenuCommand(id: number): void { + console.log("unregisterMenuCommand", id); if (!this.menuMap) { this.menuMap = new Map(); } @@ -458,4 +448,68 @@ export default class GMApi { }, }; } + + @GMContext.API() + public async GM_notification( + detail: GMTypes.NotificationDetails | string, + ondone?: GMTypes.NotificationOnDone | string, + image?: string, + onclick?: GMTypes.NotificationOnClick + ) { + let data: GMTypes.NotificationDetails = {}; + if (typeof detail === "string") { + data.text = detail; + switch (arguments.length) { + case 4: + data.onclick = onclick; + case 3: + data.image = image; + case 2: + data.title = ondone; + default: + break; + } + } else { + data = detail; + data.ondone = data.ondone || 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; + } + }); + } } diff --git a/src/runtime/content/inject.ts b/src/app/service/content/inject.ts similarity index 88% rename from src/runtime/content/inject.ts rename to src/app/service/content/inject.ts index b430572..bc42eee 100644 --- a/src/runtime/content/inject.ts +++ b/src/app/service/content/inject.ts @@ -2,7 +2,8 @@ import { ScriptRunResouce } from "@App/app/repo/scripts"; import { Message, Server } from "@Packages/message/server"; import ExecScript, { ValueUpdateData } from "./exec_script"; import { addStyle, ScriptFunc } from "./utils"; -import { getStorageName } from "../utils"; +import { getStorageName } from "@App/pkg/utils/utils"; +import { EmitEventRequest } from "../service_worker/runtime"; export class InjectRuntime { execList: ExecScript[] = []; @@ -29,11 +30,11 @@ export class InjectRuntime { }); } }); - this.server.on("runtime/menuClick", (data: { id: number; uuid: string }) => { + this.server.on("runtime/emitEvent", (data: EmitEventRequest) => { // 转发给脚本 const exec = this.execList.find((val) => val.scriptRes.uuid === data.uuid); if (exec) { - exec.menuClick(data.id); + exec.emitEvent(data.event, data.data); } }); this.server.on("runtime/valueUpdate", (data: ValueUpdateData) => { diff --git a/src/runtime/content/utils.test.ts b/src/app/service/content/utils.test.ts similarity index 100% rename from src/runtime/content/utils.test.ts rename to src/app/service/content/utils.test.ts diff --git a/src/runtime/content/utils.ts b/src/app/service/content/utils.ts similarity index 99% rename from src/runtime/content/utils.ts rename to src/app/service/content/utils.ts index f11bd12..9ae37b0 100644 --- a/src/runtime/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -64,6 +64,7 @@ export function createContext(scriptRes: ScriptRunResouce, GMInfo: any, envPrefi sendMessage: GMApi.prototype.sendMessage, connect: GMApi.prototype.connect, runFlag: uuidv4(), + eventId: 10000, valueUpdate: GMApi.prototype.valueUpdate, menuClick: GMApi.prototype.menuClick, EE: new EventEmitter(), diff --git a/src/runtime/gm_api.test.ts b/src/app/service/gm_api.test.ts similarity index 100% rename from src/runtime/gm_api.test.ts rename to src/app/service/gm_api.test.ts diff --git a/src/app/service/offscreen/index.ts b/src/app/service/offscreen/index.ts index b68b72f..ffd4c2b 100644 --- a/src/app/service/offscreen/index.ts +++ b/src/app/service/offscreen/index.ts @@ -48,7 +48,9 @@ export class OffscreenManager { script.init(); // 转发从sandbox来的gm api请求 forwardMessage("serviceWorker", "runtime/gmApi", this.windowServer, this.extensionMessage); - // 转发message queue请求 + // 转发valueUpdate与emitEvent + forwardMessage("sandbox", "runtime/valueUpdate", this.windowServer, this.windowMessage); + forwardMessage("sandbox", "runtime/emitEvent", this.windowServer, this.windowMessage); const gmApi = new GMApi(this.windowServer.group("gmApi")); gmApi.init(); diff --git a/src/app/service/sandbox/runtime.ts b/src/app/service/sandbox/runtime.ts index 9e7d9b9..24ff66d 100644 --- a/src/app/service/sandbox/runtime.ts +++ b/src/app/service/sandbox/runtime.ts @@ -7,12 +7,14 @@ import { SCRIPT_TYPE_BACKGROUND, ScriptRunResouce, } from "@App/app/repo/scripts"; -import ExecScript from "@App/runtime/content/exec_script"; -import { BgExecScriptWarp, CATRetryError } from "@App/runtime/content/exec_warp"; import { Server } from "@Packages/message/server"; import { WindowMessage } from "@Packages/message/window_message"; import { CronJob } from "cron"; import { proxyUpdateRunStatus } from "../offscreen/client"; +import { BgExecScriptWarp } from "../content/exec_warp"; +import ExecScript, { ValueUpdateData } from "../content/exec_script"; +import { getStorageName } from "@App/pkg/utils/utils"; +import { EmitEventRequest } from "../service_worker/runtime"; export class Runtime { cronJob: Map> = new Map(); @@ -290,10 +292,30 @@ export class Runtime { return this.execScript(script, true); } + valueUpdate(data: ValueUpdateData) { + // 转发给脚本 + this.execScripts.forEach((val) => { + if (val.scriptRes.uuid === data.uuid || getStorageName(val.scriptRes) === data.storageName) { + val.valueUpdate(data); + } + }); + } + + emitEvent(data: EmitEventRequest) { + // 转发给脚本 + const exec = this.execScripts.get(data.uuid); + if (exec) { + exec.emitEvent(data.event, data.data); + } + } + init() { this.api.on("enableScript", this.enableScript.bind(this)); this.api.on("disableScript", this.disableScript.bind(this)); this.api.on("runScript", this.runScript.bind(this)); this.api.on("stopScript", this.stopScript.bind(this)); + + this.api.on("runtime/valueUpdate", this.valueUpdate.bind(this)); + this.api.on("runtime/emitEvent", this.emitEvent.bind(this)); } } diff --git a/src/app/service/service_worker/gm_api.ts b/src/app/service/service_worker/gm_api.ts index e4fdccf..fa7ccd5 100644 --- a/src/app/service/service_worker/gm_api.ts +++ b/src/app/service/service_worker/gm_api.ts @@ -6,7 +6,6 @@ import { ValueService } from "@App/app/service/service_worker/value"; import PermissionVerify from "./permission_verify"; import { connect } from "@Packages/message/client"; import Cache, { incr } from "@App/app/cache"; -import { unsafeHeaders } from "@App/runtime/utils"; import EventEmitter from "eventemitter3"; import { MessageQueue } from "@Packages/message/message_queue"; import { RuntimeService } from "./runtime"; @@ -24,6 +23,35 @@ export type Request = MessageRequest & { script: Script; }; +export const unsafeHeaders: { [key: string]: boolean } = { + // 部分浏览器中并未允许 + "user-agent": true, + // 这两个是前缀 + "proxy-": true, + "sec-": true, + // cookie已经特殊处理 + cookie: true, + "accept-charset": true, + "accept-encoding": true, + "access-control-request-headers": true, + "access-control-request-method": true, + connection: true, + "content-length": true, + date: true, + dnt: true, + expect: true, + "feature-policy": true, + host: true, + "keep-alive": true, + origin: true, + referer: true, + te: true, + trailer: true, + "transfer-encoding": true, + upgrade: true, + via: true, +}; + export type Api = (request: Request, con: GetSender) => Promise; export default class GMApi { @@ -194,7 +222,7 @@ export default class GMApi { id: id, name: name, accessKey: accessKey, - tabId: sender.getSender().tab!.id!, + tabId: sender.getSender().tab?.id || -1, frameId: sender.getSender().frameId, documentId: sender.getSender().documentId, }); @@ -207,7 +235,7 @@ export default class GMApi { this.mq.emit("unregisterMenuCommand", { uuid: request.script.uuid, id: id, - tabId: sender.getSender().tab!.id!, + tabId: sender.getSender().tab?.id || -1, frameId: sender.getSender().frameId, }); } diff --git a/src/app/service/service_worker/popup.ts b/src/app/service/service_worker/popup.ts index 6d6618e..dda3332 100644 --- a/src/app/service/service_worker/popup.ts +++ b/src/app/service/service_worker/popup.ts @@ -20,7 +20,7 @@ import { subscribeScriptMenuRegister, subscribeScriptRunStatus, } from "../queue"; -import { getStorageName } from "@App/runtime/utils"; +import { getStorageName } from "@App/pkg/utils/utils"; export type ScriptMenuItem = { id: number; @@ -206,7 +206,7 @@ export class PopupService { // 事务更新脚本菜单 txUpdateScriptMenu(tabId: number, callback: (menu: ScriptMenu[]) => Promise) { - return Cache.getInstance().tx("tabScript:" + tabId, async (menu) => { + return Cache.getInstance().tx("tabScript:" + tabId, async (menu) => { return callback(menu || []); }); } @@ -252,14 +252,15 @@ export class PopupService { if (script.type === SCRIPT_TYPE_NORMAL) { return; } + if (script.status !== SCRIPT_STATUS_ENABLE) { + return; + } return this.txUpdateScriptMenu(-1, async (menu) => { const scriptMenu = menu.find((item) => item.uuid === script.uuid); - if (script.status === SCRIPT_STATUS_ENABLE) { - // 加入菜单 - if (!scriptMenu) { - const item = this.scriptToMenu(script); - menu.push(item); - } + // 加入菜单 + if (!scriptMenu) { + const item = this.scriptToMenu(script); + menu.push(item); } return menu; }); @@ -294,24 +295,22 @@ export class PopupService { const index = menu.findIndex((item) => item.uuid === uuid); if (index !== -1) { menu.splice(index, 1); - return menu; } - return null; + return menu; }); }); subscribeScriptRunStatus(this.mq, async ({ uuid, runStatus }) => { return this.txUpdateScriptMenu(-1, async (menu) => { const scriptMenu = menu.find((item) => item.uuid === uuid); if (scriptMenu) { - if (scriptMenu.runStatus === SCRIPT_RUN_STATUS_RUNNING) { + scriptMenu.runStatus = runStatus; + if (runStatus === SCRIPT_RUN_STATUS_RUNNING) { scriptMenu.runNum = 1; } else { scriptMenu.runNum = 0; } - scriptMenu.runStatus = runStatus; - return menu; } - return null; + return menu; }); }); } @@ -330,13 +329,12 @@ export class PopupService { documentId: string; }) { // 菜单点击事件 - this.runtime.sendMessageToTab( + this.runtime.EmitEventToTab( tabId, - "menuClick", { uuid, - id, - tabId, + event: "menuClick", + data: id, }, { frameId, @@ -372,14 +370,21 @@ export class PopupService { const [, , uuid, id] = menuIds; // 寻找menu信息 const menu = await this.getScriptMenu(tab!.id!); - const script = menu.find((item) => item.uuid === uuid); + let script = menu.find((item) => item.uuid === uuid); + let bgscript = false; + if (!script) { + // 从后台脚本中寻找 + const backgroundMenu = await this.getScriptMenu(-1); + script = backgroundMenu.find((item) => item.uuid === uuid); + bgscript = true; + } if (script) { const menuItem = script.menus.find((item) => item.id === parseInt(id, 10)); if (menuItem) { this.menuClick({ uuid: script.uuid, id: menuItem.id, - tabId: tab!.id!, + tabId: bgscript ? -1 : tab!.id!, frameId: menuItem.frameId || 0, documentId: menuItem.documentId || "", }); diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index ce7947b..10546fd 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -16,11 +16,11 @@ import { ScriptService } from "./script"; import { runScript, stopScript } from "../offscreen/client"; import { getRunAt } from "./utils"; import { randomString } from "@App/pkg/utils/utils"; -import { compileInjectScript } from "@App/runtime/content/utils"; import Cache from "@App/app/cache"; import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match"; import { ExtensionContentMessageSend } from "@Packages/message/extension_message"; import { sendMessage } from "@Packages/message/client"; +import { compileInjectScript } from "../content/utils"; // 为了优化性能,存储到缓存时删除了code与value export interface ScriptMatchInfo extends ScriptRunResouce { @@ -29,6 +29,12 @@ export interface ScriptMatchInfo extends ScriptRunResouce { customizeExcludeMatches: string[]; } +export interface EmitEventRequest { + uuid: string; + event: string; + data: any; +} + export class RuntimeService { scriptDAO: ScriptDAO = new ScriptDAO(); @@ -132,6 +138,22 @@ export class RuntimeService { return sendMessage(new ExtensionContentMessageSend(tabId, options), "content/runtime/" + action, data); } + // 给指定脚本触发事件 + EmitEventToTab( + tabId: number, + req: EmitEventRequest, + options?: { + documentId?: string; + frameId?: number; + } + ) { + if (tabId === -1) { + // 如果是-1, 代表给offscreen发送消息 + return sendMessage(this.sender, "offscreen/runtime/emitEvent", req); + } + return sendMessage(new ExtensionContentMessageSend(tabId, options), "content/runtime/emitEvent", req); + } + async getPageScriptUuidByUrl(url: string, includeCustomize?: boolean) { const match = await this.loadScriptMatchInfo(); // 匹配当前页面的脚本 diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 7cdb4ca..4e39629 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -19,7 +19,7 @@ import { MessageQueue } from "@Packages/message/message_queue"; import { InstallSource } from "."; import { ResourceService } from "./resource"; import { ValueService } from "./value"; -import { compileScriptCode } from "@App/runtime/content/utils"; +import { compileScriptCode } from "../content/utils"; export class ScriptService { logger: Logger; diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index ce81008..5977585 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -2,13 +2,13 @@ import LoggerCore from "@App/app/logger/core"; import Logger from "@App/app/logger/logger"; import { Script, SCRIPT_TYPE_NORMAL, ScriptDAO } from "@App/app/repo/scripts"; import { ValueDAO } from "@App/app/repo/value"; -import { getStorageName } from "@App/runtime/utils"; import { Group, MessageSend } from "@Packages/message/server"; import { RuntimeService } from "./runtime"; import { PopupService } from "./popup"; -import { ValueUpdateData, ValueUpdateSender } from "@App/runtime/content/exec_script"; import { sendMessage } from "@Packages/message/client"; import Cache from "@App/app/cache"; +import { getStorageName } from "@App/pkg/utils/utils"; +import { ValueUpdateData, ValueUpdateSender } from "../content/exec_script"; export class ValueService { logger: Logger; @@ -67,21 +67,18 @@ export class ValueService { uuid, storageName: storageName, }; - // 判断是后台脚本还是前台脚本 - if (script.type === SCRIPT_TYPE_NORMAL) { - chrome.tabs.query({}, (tabs) => { - // 推送到所有加载了本脚本的tab中 - tabs.forEach(async (tab) => { - const scriptMenu = await this.popup!.getScriptMenu(tab.id!); - if (scriptMenu.find((item) => item.storageName === storageName)) { - this.runtime!.sendMessageToTab(tab.id!, "valueUpdate", sendData); - } - }); + + chrome.tabs.query({}, (tabs) => { + // 推送到所有加载了本脚本的tab中 + tabs.forEach(async (tab) => { + const scriptMenu = await this.popup!.getScriptMenu(tab.id!); + if (scriptMenu.find((item) => item.storageName === storageName)) { + this.runtime!.sendMessageToTab(tab.id!, "valueUpdate", sendData); + } }); - } else { - // 推送到offscreen中 - sendMessage(this.send, "offscreen/runtime/valueUpdate", sendData); - } + }); + // 推送到offscreen中 + sendMessage(this.send, "offscreen/runtime/valueUpdate", sendData); return Promise.resolve(true); } diff --git a/src/content.ts b/src/content.ts index 02ddd5a..32dda36 100644 --- a/src/content.ts +++ b/src/content.ts @@ -3,8 +3,8 @@ import MessageWriter from "./app/logger/message_writer"; import { ExtensionMessage, ExtensionMessageSend } from "@Packages/message/extension_message"; import { CustomEventMessage } from "@Packages/message/custom_event_message"; import { RuntimeClient } from "./app/service/service_worker/client"; -import ContentRuntime from "./runtime/content/content"; import { Server } from "@Packages/message/server"; +import ContentRuntime from "./app/service/content/content"; // 建立与service_worker页面的连接 const send = new ExtensionMessageSend(); diff --git a/src/inject.ts b/src/inject.ts index 704e0f8..5adc78e 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -2,8 +2,8 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; import { CustomEventMessage } from "@Packages/message/custom_event_message"; import { Server } from "@Packages/message/server"; -import { InjectRuntime } from "./runtime/content/inject"; import { ScriptRunResouce } from "./app/repo/scripts"; +import { InjectRuntime } from "./app/service/content/inject"; const msg = new CustomEventMessage(MessageFlag, false); diff --git a/src/pkg/utils/monaco-editor.ts b/src/pkg/utils/monaco-editor.ts index 4237c38..81f3669 100644 --- a/src/pkg/utils/monaco-editor.ts +++ b/src/pkg/utils/monaco-editor.ts @@ -1,9 +1,11 @@ -import dts from "@App/types/scriptcat.d.ts"; +import dts from "@App/template/scriptcat.d.tpl"; import { languages } from "monaco-editor"; // 注册eslint // const linterWorker = new Worker("/src/linter.worker.js"); +console.log(dts, dts.length); + export default function registerEditor() { window.MonacoEnvironment = { getWorkerUrl(moduleId: any, label: any) { @@ -14,7 +16,7 @@ export default function registerEditor() { }, }; - languages.typescript.javascriptDefaults.addExtraLib(dts, "tampermonkey.d.ts"); + languages.typescript.javascriptDefaults.addExtraLib(dts, "scriptcat.d.ts"); // 悬停提示 const prompt: { [key: string]: any } = { diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index eae72c7..fe6b954 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -1,4 +1,4 @@ -import { Metadata } from "@App/app/repo/scripts"; +import { Metadata, Script } from "@App/app/repo/scripts"; import { CronTime } from "cron"; import dayjs from "dayjs"; import semver from "semver"; @@ -217,3 +217,10 @@ export function sleep(time: number) { setTimeout(resolve, time); }); } + +export function getStorageName(script: Script): string { + if (script.metadata && script.metadata.storagename) { + return script.metadata.storagename[0]; + } + return script.uuid; +} diff --git a/src/runtime/service_worker/runtime.ts b/src/runtime/service_worker/runtime.ts deleted file mode 100644 index decb7ce..0000000 --- a/src/runtime/service_worker/runtime.ts +++ /dev/null @@ -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 = new UrlMatch(); - - include: UrlInclude = new UrlInclude(); - - // 自定义排除 - customizeExclude: UrlMatch = new UrlMatch(); - - static hook = new Hook<"runStatus">(); - - // 运行中和开启的后台脚本 - runBackScript: Map = 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 - >(); - 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[] = 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 { - // 脚本更新先更新资源 - 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 { - // 清理匹配资源 - if (script.type === SCRIPT_TYPE_NORMAL) { - this.match.del(script); - this.include.del(script); - } else { - this.unloadBackgroundScript(script); - } - return Promise.resolve(true); - } - - // 脚本开启 - async enable(script: Script): Promise { - // 编译脚本运行资源 - const scriptRes = await this.buildScriptRunResource(script); - if (script.type !== SCRIPT_TYPE_NORMAL) { - return this.loadBackgroundScript(scriptRes); - } - return this.loadPageScript(scriptRes); - } - - // 脚本关闭 - disable(script: Script): Promise { - 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(script); - this.include.del(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(script); - } - - // 加载并启动后台脚本 - loadBackgroundScript(script: ScriptRunResouce): Promise { - 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 { - 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 { - const ret: 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); - } -} diff --git a/src/runtime/service_worker/utils.ts b/src/runtime/service_worker/utils.ts deleted file mode 100644 index 14a8366..0000000 --- a/src/runtime/service_worker/utils.ts +++ /dev/null @@ -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(); - const isRedirects = new Map(); - // 处理发送请求的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: [""], - }, - 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: [""], - }, - respOpt - ); - chrome.webRequest.onCompleted.addListener( - (details) => { - if (!isExtensionRequest(details)) { - return; - } - // 删除最大重定向数缓存 - maxRedirects.delete(details.requestId); - isRedirects.delete(details.requestId); - }, - { 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 { - 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: 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 = 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(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 -) { - 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); - } - } -} diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts deleted file mode 100644 index febb764..0000000 --- a/src/runtime/utils.ts +++ /dev/null @@ -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; -} diff --git a/src/template/scriptcat.d.tpl b/src/template/scriptcat.d.tpl new file mode 100644 index 0000000..a9d139a --- /dev/null +++ b/src/template/scriptcat.d.tpl @@ -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; + +declare function GM_download(details: GMTypes.DownloadDetails): GMTypes.AbortHandle; +declare function GM_download(url: string, filename: string): GMTypes.AbortHandle; + +declare function GM_getTab(callback: (obj: object) => unknown): void; + +declare function GM_saveTab(obj: object): Promise; + +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 = (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; + onloadstart?: Listener; + onloadend?: Listener; + onprogress?: Listener; + onreadystatechange?: Listener; + ontimeout?: () => void; + onabort?: () => void; + onerror?: (err: string) => void; + } + + interface AbortHandle { + 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; + ontimeout?: () => void; + onload?: Listener; + onprogress?: Listener; + } + + 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; + } +}