基本实现完成GM API

This commit is contained in:
王一之 2025-04-12 02:05:10 +08:00
parent 5c0d4a2560
commit d697928fb0
20 changed files with 631 additions and 99 deletions

View File

@ -1,5 +1,5 @@
// ==UserScript==
// @name New Userscript
// @name GM cookie操作
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1.0
// @description 可以控制浏览器的cookie, 必须指定@connect, 并且每次一个新的域调用都需要用户确定
@ -9,8 +9,6 @@
// @connect example.com
// ==/UserScript==
// GM_cookie("store") 方法请看storage_name/gm_value.js的例子, 可用于隐身窗口的操作
GM_cookie("set", {
url: "http://example.com/cookie",
name: "cookie1", value: "value"

View File

@ -9,7 +9,7 @@
// ==/UserScript==
GM_download({
url: "https://scriptcat.org/api/v1/gm_crx/download/ScriptCat",
url: "https://scriptcat.org/api/v2/open/crx-download/ndcooeababalnlpkfedmmbbbgkljhpjf",
name: "scriptcat.crx",
headers: {
"referer": "http://www.example.com/",

View File

@ -13,13 +13,8 @@
// @storageName example
// ==/UserScript==
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote, tabid) {
console.log("test_set change", name, oldval, newval, remote, tabid);
// 可以通过tabid获取到触发变化的tab
// GM_cookie.store可以获取到对应的cookie storeId
GM_cookie("store", tabid, (storeId) => {
console.log("store", storeId);
});
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote) {
console.log("test_set change", name, oldval, newval, remote);
});
setInterval(() => {

View File

@ -14,13 +14,8 @@
// ==/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);
});
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote) {
console.log("value change", name, oldval, newval, remote);
});
setInterval(() => {

View File

@ -1,6 +1,6 @@
{
"name": "scriptcat",
"version": "0.17.0-alpha.2",
"version": "0.17.0-alpha.1",
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
"author": "CodFrm",
"license": "GPLv3",

View File

@ -6,9 +6,9 @@ export async function sendMessage(msg: MessageSend, action: string, data?: any):
LoggerCore.getInstance().logger().trace("sendMessage", { action, data, response: res });
if (res && res.code) {
console.error(res);
return Promise.reject(res.message);
throw res.message;
} else {
return Promise.resolve(res.data);
return res.data;
}
}

View File

@ -138,20 +138,7 @@ export function forwardMessage(
to: MessageSend,
middleware?: ApiFunctionSync
) {
from.on(path, (params, fromCon) => {
if (middleware) {
// 此处是为了处理CustomEventMessage的同步消息情况
const resp = middleware(params, fromCon) as any;
if (resp instanceof Promise) {
return resp.then((data) => {
if (data !== false) {
return data;
}
});
} else if (resp !== false) {
return resp;
}
}
const handler = (params: any, fromCon: GetSender) => {
const fromConnect = fromCon.getConnect();
if (fromConnect) {
connect(to, prefix + "/" + path, params).then((toCon) => {
@ -171,5 +158,22 @@ export function forwardMessage(
} else {
return sendMessage(to, prefix + "/" + path, params);
}
};
from.on(path, (params, sender) => {
if (middleware) {
// 此处是为了处理CustomEventMessage的同步消息情况
const resp = middleware(params, sender) as any;
if (resp instanceof Promise) {
return resp.then((data) => {
if (data !== false) {
return data;
}
return handler(params, sender);
});
} else if (resp !== false) {
return resp;
}
return handler(params, sender);
}
});
}

View File

@ -116,7 +116,7 @@ export default class Cache {
ret = await set();
this.set(key, ret);
}
return Promise.resolve(ret);
return ret;
}
public set(key: string, value: any): Promise<void> {
@ -173,8 +173,9 @@ export default class Cache {
if (value) {
newValue = value;
return this.set(key, value);
} else if (value === undefined) {
return this.del(key);
}
return Promise.resolve();
});
unlock();
return newValue!;

View File

@ -75,9 +75,9 @@ export abstract class DAO<T> {
}
const resp = await this.table.update(id, <any>val);
if (resp) {
return Promise.resolve(id);
return id;
}
return Promise.reject(ErrSaveError);
throw ErrSaveError;
}
public findById(id: number) {

View File

@ -35,7 +35,7 @@ export default class ContentRuntime {
setTimeout(() => {
URL.revokeObjectURL(url);
}, 60 * 1000);
return Promise.resolve(url);
return url;
}
case "CAT_fetchBlob": {
return fetch(data.params[0]).then((res) => res.blob());
@ -80,7 +80,7 @@ export default class ContentRuntime {
return nodeId;
}
}
return Promise.resolve(false);
return false;
}
);
const client = new Client(this.msg, "inject");

View File

@ -215,7 +215,22 @@ export default class GMApi {
@GMContext.API()
public async CAT_fetchDocument(url: string): Promise<Document | undefined> {
const data = await this.sendMessage("CAT_fetchDocument", [url]);
return (<CustomEventMessage>this.message).getAndDelRelatedTarget(data.relatedTarget);
return (<CustomEventMessage>this.message).getAndDelRelatedTarget(data.relatedTarget) as Document;
}
@GMContext.API()
GM_cookie(
action: string,
details: GMTypes.CookieDetails,
done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void
) {
this.sendMessage("GM_cookie", [action, details])
.then((resp: any) => {
done && done(resp, undefined);
})
.catch((err) => {
done && done(undefined, err);
});
}
@GMContext.API()
@ -479,7 +494,7 @@ export default class GMApi {
break;
default:
LoggerCore.logger().warn("GM_xmlhttpRequest resp is error", {
action: data.action,
data,
});
break;
}
@ -497,6 +512,64 @@ export default class GMApi {
};
}
@GMContext.API()
GM_download(url: GMTypes.DownloadDetails | string, filename?: string): GMTypes.AbortHandle<void> {
let details: GMTypes.DownloadDetails;
if (typeof url === "string") {
details = {
name: filename || "",
url,
};
} else {
details = url;
}
let connect: MessageConnect;
this.connect("GM_download", [
{
method: details.method,
url: details.url,
name: details.name,
headers: details.headers,
saveAs: details.saveAs,
timeout: details.timeout,
cookie: details.cookie,
anonymous: details.anonymous,
},
]).then((con) => {
connect = con;
connect.onMessage((data: { action: string; data: any }) => {
switch (data.action) {
case "onload":
details.onload && details.onload(data.data);
break;
case "onprogress":
details.onprogress && details.onprogress(<GMTypes.XHRProgress>data.data);
break;
case "ontimeout":
details.ontimeout && details.ontimeout();
break;
case "onerror":
details.onerror &&
details.onerror({
error: "unknown",
});
break;
default:
LoggerCore.logger().warn("GM_download resp is error", {
data,
});
break;
}
});
});
return {
abort: () => {
connect?.disconnect();
},
};
}
@GMContext.API({
depend: ["GM_closeNotification", "GM_updateNotification"],
})
@ -576,6 +649,88 @@ export default class GMApi {
this.sendMessage("GM_updateNotification", [id, details]);
}
@GMContext.API({ depend: ["GM_closeInTab"] })
public GM_openInTab(url: string, options?: GMTypes.OpenTabOptions | boolean): GMTypes.Tab {
let option: GMTypes.OpenTabOptions = {};
if (arguments.length === 1) {
option.active = true;
} else if (typeof options === "boolean") {
option.active = !options;
} else {
option = <GMTypes.OpenTabOptions>options;
}
if (option.active === undefined) {
option.active = true;
}
let tabid: any;
const ret: GMTypes.Tab = {
close: () => {
tabid && this.GM_closeInTab(tabid);
},
};
this.sendMessage("GM_openInTab", [url, option]).then((id) => {
if (id) {
tabid = id;
this.EE.addListener("GM_openInTab:" + id, (resp: any) => {
switch (resp.event) {
case "oncreate":
tabid = resp.tabId;
break;
case "onclose":
ret.onclose && ret.onclose();
ret.closed = true;
this.EE.removeAllListeners("GM_openInTab:" + id);
break;
default:
LoggerCore.logger().warn("GM_openInTab resp is error", {
resp,
});
break;
}
});
} else {
ret.onclose && ret.onclose();
ret.closed = true;
}
});
return ret;
}
@GMContext.API()
public GM_closeInTab(tabid: string) {
return this.sendMessage("GM_closeInTab", [tabid]);
}
@GMContext.API()
GM_getTab(callback: (data: any) => void) {
this.sendMessage("GM_getTab", []).then((data) => {
callback(data);
});
}
@GMContext.API()
GM_saveTab(obj: object) {
if (typeof obj === "object") {
obj = JSON.parse(JSON.stringify(obj));
}
this.sendMessage("GM_saveTab", [obj]);
}
@GMContext.API()
GM_getTabs(callback: (objs: { [key: string | number]: object }) => any) {
this.sendMessage("GM_getTabs", []).then((resp) => {
callback(resp);
});
}
@GMContext.API()
GM_setClipboard(data: string, info?: string | { type?: string; minetype?: string }) {
this.sendMessage("GM_setClipboard", [data, info]);
}
@GMContext.API()
GM_getResourceURL(name: string, isBlobUrl?: boolean): string | undefined {
if (!this.scriptRes.resource) {

View File

@ -15,7 +15,6 @@ export default class GMApi {
data?: any
) {
const finalUrl = xhr.responseURL || details.url;
// 判断是否有headerFlag-final-url,有则替换finalUrl
let response: GMTypes.XHRResponse = {
finalUrl,
readyState: <any>xhr.readyState,
@ -173,7 +172,38 @@ export default class GMApi {
});
}
openInTab({ url }: { url: string }) {
return Promise.resolve(window.open(url) !== undefined);
}
textarea: HTMLTextAreaElement = document.createElement("textarea");
clipboardData: { type?: string; data: string } | undefined;
async setClipboard({ data, type }: { data: string; type: string }) {
this.clipboardData = {
type,
data,
};
this.textarea.focus();
document.execCommand("copy", false, <any>null);
}
init() {
this.textarea.style.display = "none";
document.documentElement.appendChild(this.textarea);
document.addEventListener("copy", (e: ClipboardEvent) => {
if (!this.clipboardData || !e.clipboardData) {
return;
}
e.preventDefault();
const { type, data } = this.clipboardData;
e.clipboardData.setData(type || "text/plain", data);
this.clipboardData = undefined;
});
this.group.on("xmlHttpRequest", this.xmlHttpRequest.bind(this));
this.group.on("openInTab", this.openInTab.bind(this));
this.group.on("setClipboard", this.setClipboard.bind(this));
}
}

View File

@ -206,7 +206,7 @@ export class Runtime {
} else {
this.cronJob.set(script.uuid, cronJobList);
}
return Promise.resolve(!flag);
return !flag;
}
crontabExec(script: ScriptRunResouce, oncePos: number) {

View File

@ -4,12 +4,15 @@ import { Script, ScriptDAO } from "@App/app/repo/scripts";
import { ExtMessageSender, GetSender, Group, MessageSend } from "@Packages/message/server";
import { ValueService } from "@App/app/service/service_worker/value";
import PermissionVerify from "./permission_verify";
import { connect } from "@Packages/message/client";
import { connect, sendMessage } from "@Packages/message/client";
import Cache, { incr } from "@App/app/cache";
import EventEmitter from "eventemitter3";
import { MessageQueue } from "@Packages/message/message_queue";
import { RuntimeService } from "./runtime";
import { getIcon, isFirefox } from "@App/pkg/utils/utils";
import { PopupService } from "./popup";
import { act } from "react";
import { MockMessageConnect } from "@Packages/message/mock_message";
// GMApi,处理脚本的GM API调用请求
@ -82,14 +85,14 @@ export default class GMApi {
this.logger.trace("GM API request", { api: data.api, uuid: data.uuid, param: data.params });
const api = PermissionVerify.apis.get(data.api);
if (!api) {
return Promise.reject(new Error("gm api is not found"));
throw new Error("gm api is not found");
}
const req = await this.parseRequest(data);
try {
await this.permissionVerify.verify(req, api);
} catch (e) {
this.logger.error("verify error", { api: data.api }, Logger.E(e));
return Promise.reject(e);
throw e;
}
return api.api.call(this, req, sender);
}
@ -98,17 +101,103 @@ export default class GMApi {
async parseRequest(data: MessageRequest): Promise<Request> {
const script = await this.scriptDAO.get(data.uuid);
if (!script) {
return Promise.reject(new Error("script is not found"));
throw new Error("script is not found");
}
const req: Request = <Request>data;
req.script = script;
return Promise.resolve(req);
return req;
}
@PermissionVerify.API()
async GM_cookie(request: Request, sender: GetSender) {
const param = request.params;
if (param.length !== 2) {
throw new Error("there must be two parameters");
}
const detail = <GMTypes.CookieDetails>request.params[1];
// url或者域名不能为空
if (detail.url) {
detail.url = detail.url.trim();
}
if (detail.domain) {
detail.domain = detail.domain.trim();
}
if (!detail.url && !detail.domain) {
throw new Error("there must be one of url or domain");
}
// 处理tab的storeid
let tabId = sender.getExtMessageSender().tabId;
let storeId: string | undefined;
if (tabId !== -1) {
const stores = await chrome.cookies.getAllCookieStores();
const store = stores.find((val) => val.tabIds.includes(tabId));
if (store) {
storeId = store.id;
}
}
switch (param[0]) {
case "list": {
return chrome.cookies.getAll({
domain: detail.domain,
name: detail.name,
path: detail.path,
secure: detail.secure,
session: detail.session,
url: detail.url,
storeId: storeId,
});
}
case "delete": {
if (!detail.url || !detail.name) {
throw new Error("delete operation must have url and name");
}
await chrome.cookies.remove({
name: detail.name,
url: detail.url,
storeId: storeId,
});
break;
}
case "set": {
if (!detail.url || !detail.name) {
throw new Error("set operation must have name and value");
}
await chrome.cookies.set({
url: detail.url,
name: detail.name,
domain: detail.domain,
value: detail.value,
expirationDate: detail.expirationDate,
path: detail.path,
httpOnly: detail.httpOnly,
secure: detail.secure,
storeId: storeId,
});
break;
}
default: {
throw new Error("action can only be: get, set, delete, store");
}
}
}
@PermissionVerify.API()
GM_log(request: Request): Promise<boolean> {
const message = request.params[0];
const level = request.params[1] || "info";
const labels = request.params[2] || {};
LoggerCore.logger(labels).log(level, message, {
uuid: request.uuid,
name: request.script.name,
component: "GM_log",
});
return Promise.resolve(true);
}
@PermissionVerify.API()
async GM_setValue(request: Request, sender: GetSender) {
if (!request.params || request.params.length !== 2) {
return Promise.reject(new Error("param is failed"));
throw new Error("param is failed");
}
const [key, value] = request.params;
await this.value.setValue(request.script.uuid, key, value, {
@ -122,7 +211,7 @@ export default class GMApi {
// 检查是否有unsafe header,有则生成dnr规则
const headers = params.headers;
if (!headers) {
return Promise.resolve({});
return {};
}
const requestHeaders = [
{
@ -151,6 +240,26 @@ export default class GMApi {
delete headers[key];
}
});
// 判断是否是anonymous
if (params.anonymous) {
// 如果是anonymous并且有cookie则设置为自定义的cookie
if (params.cookie) {
requestHeaders.push({
header: "cookie",
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
value: params.cookie,
});
} else {
// 否则删除cookie
requestHeaders.push({
header: "cookie",
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE,
});
}
} else if (params.cookie) {
// 否则正常携带cookie header
headers["cookie"] = params.cookie;
}
const ruleId = reqeustId;
const rule = {} as chrome.declarativeNetRequest.Rule;
rule.id = ruleId;
@ -176,16 +285,83 @@ export default class GMApi {
removeRuleIds: [ruleId],
addRules: [rule],
});
return Promise.resolve(headers);
return headers;
}
gmXhrHeadersReceived: EventEmitter = new EventEmitter();
dealFetch(config: GMSend.XHRDetails, response: Response, readyState: 0 | 1 | 2 | 3 | 4) {
let respHeader = "";
response.headers.forEach((value, key) => {
respHeader += `${key}: ${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;
}
CAT_fetch(config: GMSend.XHRDetails, con: GetSender, resultParam: { requestId: number; responseHeader: string }) {
const { url } = config;
let connect = con.getConnect();
return fetch(url, {
method: config.method || "GET",
body: <any>config.data,
headers: config.headers,
}).then((resp) => {
const send = this.dealFetch(config, resp, 1);
const reader = resp.body?.getReader();
if (!reader) {
throw new Error("read is not found");
}
const _this = this;
reader.read().then(function read({ done, value }) {
if (done) {
const data = _this.dealFetch(config, resp, 4);
data.responseHeaders = resultParam.responseHeader || data.responseHeaders;
connect.sendMessage({
action: "onreadystatechange",
data: data,
});
connect.sendMessage({
action: "onload",
data: data,
});
connect.sendMessage({
action: "onloadend",
data: data,
});
} else {
connect.sendMessage({
action: "onstream",
data: Array.from(value),
});
reader.read().then(read);
}
});
send.responseHeaders = resultParam.responseHeader || send.responseHeaders;
connect.sendMessage({
action: "onloadstart",
data: send,
});
send.readyState = 2;
connect.sendMessage({
action: "onreadystatechange",
data: send,
});
});
}
// TODO: maxRedirects实现
@PermissionVerify.API()
async GM_xmlhttpRequest(request: Request, con: GetSender) {
async GM_xmlhttpRequest(request: Request, sender: GetSender) {
if (request.params.length === 0) {
return Promise.reject(new Error("param is failed"));
throw new Error("param is failed");
}
const params = request.params[0] as GMSend.XHRDetails;
// 先处理unsafe hearder
@ -197,26 +373,35 @@ export default class GMApi {
}
params.headers["X-Scriptcat-GM-XHR-Request-Id"] = requestId.toString();
params.headers = await this.buildDNRRule(requestId, request.params[0]);
let responseHeader = "";
let resultParam = {
requestId,
responseHeader: "",
};
// 等待response
this.gmXhrHeadersReceived.addListener(
"headersReceived:" + requestId,
(details: chrome.webRequest.WebResponseHeadersDetails) => {
details.responseHeaders?.forEach((header) => {
responseHeader += header.name + ": " + header.value + "\r\n";
resultParam.responseHeader += header.name + ": " + header.value + "\n";
});
this.gmXhrHeadersReceived.removeAllListeners("headersReceived:" + requestId);
}
);
if (params.responseType === "stream" || params.fetch || params.redirect) {
// 只有fetch支持ReadableStream、redirect这些直接使用fetch
return this.CAT_fetch(params, sender, resultParam);
}
// 再发送到offscreen, 处理请求
const offscreenCon = await connect(this.send, "offscreen/gmApi/xmlHttpRequest", request.params[0]);
offscreenCon.onMessage((msg: { action: string; data: any }) => {
// 发送到content
// 替换msg.data.responseHeaders
if (responseHeader) {
msg.data.responseHeaders = responseHeader;
}
con.getConnect().sendMessage(msg);
msg.data.responseHeaders = resultParam.responseHeader || msg.data.responseHeaders;
sender.getConnect().sendMessage(msg);
});
sender.getConnect().onDisconnect(() => {
// 关闭连接
offscreenCon.disconnect();
});
}
@ -247,10 +432,83 @@ export default class GMApi {
});
}
@PermissionVerify.API({})
async GM_openInTab(request: Request, sender: GetSender) {
const url = request.params[0];
const options = request.params[1] || {};
if (options.useOpen === true) {
// 发送给offscreen页面处理
const ok = await sendMessage(this.send, "offscreen/gmApi/openInTab", { url });
if (ok) {
// 由于window.open强制在前台打开标签因此获取状态为{ active:true }的标签即为新标签
const [tab] = await chrome.tabs.query({ active: true });
await Cache.getInstance().set(`GM_openInTab:${tab.id}`, {
uuid: request.uuid,
sender: sender.getExtMessageSender(),
});
return tab.id;
} else {
// 当新tab被浏览器阻止时window.open()会返回null 视为已经关闭
// 似乎在Firefox中禁止在background页面使用window.open()强制返回null
return false;
}
} else {
const tab = await chrome.tabs.create({ url, active: options.active });
await Cache.getInstance().set(`GM_openInTab:${tab.id}`, {
uuid: request.uuid,
sender: sender.getExtMessageSender(),
});
return tab.id;
}
}
@PermissionVerify.API({
link: "GM_openInTab",
})
async GM_closeInTab(request: Request): Promise<boolean> {
try {
await chrome.tabs.remove(<number>request.params[0]);
} catch (e) {
this.logger.error("GM_closeInTab", Logger.E(e));
}
return Promise.resolve(true);
}
@PermissionVerify.API({})
GM_getTab(request: Request, sender: GetSender) {
return Cache.getInstance()
.tx(`GM_getTab:${request.uuid}`, async (tabData: { [key: number]: any }) => {
return tabData || {};
})
.then((data) => {
return data[sender.getExtMessageSender().tabId];
});
}
@PermissionVerify.API()
GM_saveTab(request: Request, sender: GetSender) {
const data = request.params[0];
const tabId = sender.getExtMessageSender().tabId;
return Cache.getInstance()
.tx(`GM_getTab:${request.uuid}`, (tabData: { [key: number]: any }) => {
tabData = tabData || {};
tabData[tabId] = data;
return Promise.resolve(tabData);
})
.then(() => true);
}
@PermissionVerify.API()
GM_getTabs(request: Request) {
return Cache.getInstance().tx(`GM_getTab:${request.uuid}`, async (tabData: { [key: number]: any }) => {
return tabData || {};
});
}
@PermissionVerify.API({})
GM_notification(request: Request, sender: GetSender) {
if (request.params.length === 0) {
return Promise.reject(new Error("param is failed"));
throw new Error("param is failed");
}
const details: GMTypes.NotificationDetails = request.params[0];
const options: chrome.notifications.NotificationOptions<true> = {
@ -288,7 +546,7 @@ export default class GMApi {
})
GM_closeNotification(request: Request) {
if (request.params.length === 0) {
return Promise.reject(new Error("param is failed"));
throw new Error("param is failed");
}
const [notificationId] = request.params;
Cache.getInstance().del(`GM_notification:${notificationId}`);
@ -300,7 +558,7 @@ export default class GMApi {
})
GM_updateNotification(request: Request) {
if (isFirefox()) {
return Promise.reject(new Error("firefox does not support this method"));
throw new Error("firefox does not support this method");
}
const id = request.params[0];
const details: GMTypes.NotificationDetails = request.params[1];
@ -315,6 +573,91 @@ export default class GMApi {
chrome.notifications.update(<string>id, options);
}
@PermissionVerify.API()
async GM_download(request: Request, sender: GetSender) {
const params = <GMTypes.DownloadDetails>request.params[0];
// blob本地文件直接下载
if (params.url.startsWith("blob:")) {
chrome.downloads.download(
{
url: params.url,
saveAs: params.saveAs,
filename: params.name,
},
() => {
sender.getConnect().sendMessage({ event: "onload" });
}
);
return;
}
// 使用xhr下载blob,再使用download api创建下载
const EE = new EventEmitter();
const mockConnect = new MockMessageConnect(EE);
EE.addListener("message", (data: any) => {
const xhr = data.data;
const respond: any = {
finalUrl: xhr.url,
readyState: xhr.readyState,
status: xhr.status,
statusText: xhr.statusText,
responseHeaders: xhr.responseHeaders,
};
switch (data.action) {
case "onload":
sender.getConnect().sendMessage({
action: "onload",
data: respond,
});
chrome.downloads.download({
url: xhr.response,
saveAs: params.saveAs,
filename: params.name,
});
break;
case "onerror":
sender.getConnect().sendMessage({
action: "onerror",
data: respond,
});
break;
case "onprogress":
respond.done = xhr.DONE;
respond.lengthComputable = xhr.lengthComputable;
respond.loaded = xhr.loaded;
respond.total = xhr.total;
respond.totalSize = xhr.total;
sender.getConnect().sendMessage({
action: "onprogress",
data: respond,
});
break;
case "ontimeout":
sender.getConnect().sendMessage({
action: "ontimeout",
});
break;
}
});
// 处理参数问题
request.params[0] = {
method: params.method || "GET",
url: params.url,
headers: params.headers,
timeout: params.timeout,
cookie: params.cookie,
anonymous: params.anonymous,
responseType: "blob",
} as GMSend.XHRDetails;
return this.GM_xmlhttpRequest(request, new GetSender(mockConnect));
}
@PermissionVerify.API()
async GM_setClipboard(request: Request) {
let [data, type] = request.params;
type = type || "text/plain";
await sendMessage(this.send, "offscreen/gmApi/setClipboard", { data, type });
}
handlerNotification() {
const send = async (event: string, notificationId: string, params?: any) => {
const ret = (await Cache.getInstance().get(`GM_notification:${notificationId}`)) as NotificationData;
@ -397,5 +740,25 @@ export default class GMApi {
this.group.on("gmApi", this.handlerRequest.bind(this));
this.handlerGmXhr();
this.handlerNotification();
chrome.tabs.onRemoved.addListener(async (tabId) => {
// 处理GM_openInTab关闭事件
const sender = (await Cache.getInstance().get(`GM_openInTab:${tabId}`)) as {
uuid: string;
sender: ExtMessageSender;
};
if (sender) {
this.runtime.emitEventToTab(sender.sender, {
event: "GM_openInTab",
eventId: tabId.toString(),
uuid: sender.uuid,
data: {
event: "onclose",
tabId: tabId,
},
});
Cache.getInstance().del(`GM_openInTab:${tabId}`);
}
});
}
}

View File

@ -36,8 +36,6 @@ export interface ApiParam {
background?: boolean;
// 是否需要弹出页面让用户进行确认
confirm?: (request: Request) => Promise<boolean | ConfirmParam>;
// 监听方法
listener?: () => void;
// 别名
alias?: string[];
// 关联
@ -59,9 +57,6 @@ export default class PermissionVerify {
public static API(param: ApiParam = {}) {
return (target: any, propertyName: string, descriptor: PropertyDescriptor) => {
const key = propertyName;
if (param.listener) {
param.listener();
}
PermissionVerify.apis.set(key, {
api: descriptor.value,
param,

View File

@ -183,8 +183,10 @@ export class PopupService {
const scriptMenu = script.map((script) => {
const run = runScript.find((item) => item.uuid === script.uuid);
if (run) {
// 如果脚本已经存在,则不添加,赋值状态
// 如果脚本已经存在,则不添加,更新信息
run.enable = script.status === SCRIPT_STATUS_ENABLE;
run.customExclude = script.customizeExcludeMatches || run.customExclude;
run.hasUserConfig = !!script.config;
return run;
}
return this.scriptToMenu(script);
@ -196,6 +198,7 @@ export class PopupService {
scriptMenu.push(script);
}
});
console.log("popup脚本菜单", runScript);
// 后台脚本只显示开启或者运行中的脚本
return { scriptList: scriptMenu, backScriptList: await this.getScriptMenu(-1) };
}
@ -335,9 +338,19 @@ export class PopupService {
// 监听tab开关
chrome.tabs.onRemoved.addListener((tabId) => {
// 清理数据
this.txUpdateScriptMenu(tabId, async () => {
return [];
// 清理数据tab关闭需要释放的数据
this.txUpdateScriptMenu(tabId, async (script) => {
script.forEach((script) => {
// 处理GM_saveTab关闭事件, 由于需要用到tab相关的脚本数据所以需要在这里处理
// 避免先删除了数据获取不到
Cache.getInstance().tx(`GM_getTab:${script.uuid}`, (tabData: { [key: number]: any }) => {
if (tabData) {
delete tabData[tabId];
}
return Promise.resolve(tabData);
});
});
return undefined;
});
});
// 监听页面切换加载菜单

View File

@ -21,6 +21,7 @@ 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";
import { PopupService } from "./popup";
// 为了优化性能存储到缓存时删除了code与value
export interface ScriptMatchInfo extends ScriptRunResouce {

View File

@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "__MSG_scriptcat__",
"version": "0.17.0.1003",
"version": "0.17.0.1002",
"author": "CodFrm",
"description": "__MSG_scriptcat_description__",
"options_ui": {
@ -24,13 +24,16 @@
"permissions": [
"tabs",
"storage",
"cookies",
"offscreen",
"scripting",
"downloads",
"activeTab",
"webRequest",
"userScripts",
"contextMenus",
"notifications",
"clipboardWrite",
"unlimitedStorage",
"declarativeNetRequest"
],

3
src/types/main.d.ts vendored
View File

@ -30,7 +30,8 @@ declare namespace GMSend {
password?: string;
nocache?: boolean;
dataType?: "FormData" | "Blob";
maxRedirects?: number;
redirect?: "follow" | "error" | "manual";
maxRedirects?: number; // 为了与tm保持一致, 在v0.17.0后废弃, 使用redirect替代
}
interface XHRFormData {

View File

@ -139,21 +139,6 @@ declare function GM_cookie(
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版本中添加
@ -289,10 +274,7 @@ declare namespace CATType {
}
declare namespace GMTypes {
/*
* store为获取隐身窗口之类的cookie,API,
*/
type CookieAction = "list" | "delete" | "set" | "store";
type CookieAction = "list" | "delete" | "set";
type LoggerLevel = "debug" | "info" | "warn" | "error";
@ -308,17 +290,13 @@ declare namespace GMTypes {
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;
@ -341,7 +319,7 @@ declare namespace GMTypes {
active?: boolean;
insert?: boolean;
setParent?: boolean;
useOpen?: boolean; // 这是一个实验性/不兼容其他管理器/不兼容Firefox的功能
useOpen?: boolean; // 这是一个实验性/不兼容其他管理器/不兼容Firefox的功能 表示使用window.open打开新窗口 #178
}
interface XHRResponse {