Compare commits
13 Commits
239f961485
...
44e1449e03
Author | SHA1 | Date | |
---|---|---|---|
44e1449e03 | |||
1a531dfad5 | |||
071e728f06 | |||
44b6f11b19 | |||
2a0286e47d | |||
c7763227d0 | |||
b76a685988 | |||
3b2e72127f | |||
1965137191 | |||
d697928fb0 | |||
5c0d4a2560 | |||
7ca85801ef | |||
a2870eb18e |
@@ -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"
|
||||
|
@@ -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/",
|
||||
|
@@ -1,33 +0,0 @@
|
||||
// ==UserScript==
|
||||
// @name gm value
|
||||
// @namespace https://bbs.tampermonkey.net.cn/
|
||||
// @version 0.1.0
|
||||
// @description 可以持久化存储数据, 并且可以监听数据变化
|
||||
// @author You
|
||||
// @match https://bbs.tampermonkey.net.cn/
|
||||
// @run-at document-start
|
||||
// @grant GM_setValue
|
||||
// @grant GM_getValue
|
||||
// @grant GM_addValueChangeListener
|
||||
// @grant GM_listValues
|
||||
// @grant GM_deleteValue
|
||||
// ==/UserScript==
|
||||
|
||||
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote, tabid) {
|
||||
console.log("test_set change", name, oldval, newval, remote, tabid);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
console.log(GM_getValue("test_set"));
|
||||
console.log(GM_listValues());
|
||||
}, 2000);
|
||||
|
||||
setTimeout(() => {
|
||||
GM_deleteValue("test_set");
|
||||
}, 3000);
|
||||
|
||||
GM_setValue("test_set", new Date().getTime());
|
||||
|
||||
console.log(GM_getValue("test_set2"));
|
||||
|
||||
GM_setValue("test_set2", new Date().getTime());
|
17
example/gm_value/gm_value_1_bg.js
Normal file
17
example/gm_value/gm_value_1_bg.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// ==UserScript==
|
||||
// @name gm value storage 设置方 - 定时脚本
|
||||
// @namespace https://bbs.tampermonkey.net.cn/
|
||||
// @version 0.1.0
|
||||
// @description 多个脚本之间共享数据 设置方 - 定时脚本
|
||||
// @author You
|
||||
// @run-at document-start
|
||||
// @grant GM_setValue
|
||||
// @grant GM_deleteValue
|
||||
// @storageName example
|
||||
// @crontab */5 * * * * *
|
||||
// ==/UserScript==
|
||||
|
||||
return new Promise((resolve) => {
|
||||
GM_setValue("test_set", new Date().getTime());
|
||||
resolve();
|
||||
});
|
@@ -13,16 +13,11 @@
|
||||
// @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(() => {
|
||||
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);
|
27
example/gm_value/gm_value_2_bg.js
Normal file
27
example/gm_value/gm_value_2_bg.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// ==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) {
|
||||
console.log("value change", name, oldval, newval, remote);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
console.log("test_set: ", GM_getValue("test_set"));
|
||||
console.log("value list:", GM_listValues());
|
||||
}, 2000);
|
||||
// 永不返回resolve表示永不结束
|
||||
// resolve()
|
||||
});
|
@@ -26,6 +26,7 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dexie": "^4.0.10",
|
||||
"eslint-linter-browserify": "^7.32.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"i18next": "^23.16.4",
|
||||
"monaco-editor": "^0.52.2",
|
||||
@@ -39,6 +40,7 @@
|
||||
"react-router-dom": "^7.1.1",
|
||||
"semver": "^7.6.3",
|
||||
"uuid": "^11.0.3",
|
||||
"webdav": "^5.8.0",
|
||||
"yaml": "^2.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -63,11 +65,13 @@
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-userscripts": "^0.2.12",
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mock-xmlhttprequest": "^8.4.1",
|
||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"prettier": "^3.5.3",
|
||||
|
@@ -14,6 +14,7 @@ const compatMap = {
|
||||
GM_addElement: [
|
||||
{ type: "tampermonkey", versionConstraint: ">=4.11.6113" },
|
||||
{ type: "violentmonkey", versionConstraint: ">=2.13.0-beta.3" },
|
||||
{ type: "scriptcat", versionConstraint: "*" },
|
||||
],
|
||||
"GM.addStyle": [
|
||||
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||
@@ -24,15 +25,19 @@ const compatMap = {
|
||||
{ type: "violentmonkey", versionConstraint: "*" },
|
||||
{ type: "greasemonkey", versionConstraint: ">=0.6.1.4 <4" },
|
||||
],
|
||||
"GM.addValueChangeListener": [
|
||||
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||
],
|
||||
"GM.addValueChangeListener": [{ type: "tampermonkey", versionConstraint: ">=4.5" }],
|
||||
GM_addValueChangeListener: [
|
||||
{ type: "tampermonkey", versionConstraint: ">=2.3.2607" },
|
||||
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||
],
|
||||
"GM.cookie": [{ type: "tampermonkey", versionConstraint: ">=4.8" }],
|
||||
GM_cookie: [{ type: "tampermonkey", versionConstraint: ">=4.8" }],
|
||||
"GM.cookie": [
|
||||
{ type: "tampermonkey", versionConstraint: ">=4.8" },
|
||||
{ type: "scriptcat", versionConstraint: "*" },
|
||||
],
|
||||
GM_cookie: [
|
||||
{ type: "tampermonkey", versionConstraint: ">=4.8" },
|
||||
{ type: "scriptcat", versionConstraint: "*" },
|
||||
],
|
||||
"GM.deleteValue": [
|
||||
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||
@@ -54,9 +59,7 @@ const compatMap = {
|
||||
{ type: "violentmonkey", versionConstraint: "*" },
|
||||
{ type: "greasemonkey", versionConstraint: ">=0.8.20080609.0 <4" },
|
||||
],
|
||||
"GM.getResourceURL": [
|
||||
{ type: "violentmonkey", versionConstraint: ">=2.12.0 <2.13.0.10" },
|
||||
],
|
||||
"GM.getResourceURL": [{ type: "violentmonkey", versionConstraint: ">=2.12.0 <2.13.0.10" }],
|
||||
GM_getResourceURL: [
|
||||
{ type: "tampermonkey", versionConstraint: "*" },
|
||||
{ type: "violentmonkey", versionConstraint: "*" },
|
||||
@@ -139,9 +142,7 @@ const compatMap = {
|
||||
{ type: "violentmonkey", versionConstraint: "*" },
|
||||
{ type: "greasemonkey", versionConstraint: ">=0.2.5 <4" },
|
||||
],
|
||||
"GM.removeValueChangeListener": [
|
||||
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||
],
|
||||
"GM.removeValueChangeListener": [{ type: "tampermonkey", versionConstraint: ">=4.5" }],
|
||||
GM_removeValueChangeListener: [
|
||||
{ type: "tampermonkey", versionConstraint: ">=2.3.2607" },
|
||||
{ type: "violentmonkey", versionConstraint: ">=2.12.0" },
|
||||
@@ -168,9 +169,7 @@ const compatMap = {
|
||||
{ type: "violentmonkey", versionConstraint: "*" },
|
||||
{ type: "greasemonkey", versionConstraint: ">=0.3-beta <4" },
|
||||
],
|
||||
"GM.unregisterMenuCommand": [
|
||||
{ type: "tampermonkey", versionConstraint: ">=4.5" },
|
||||
],
|
||||
"GM.unregisterMenuCommand": [{ type: "tampermonkey", versionConstraint: ">=4.5" }],
|
||||
GM_unregisterMenuCommand: [
|
||||
{ type: "tampermonkey", versionConstraint: ">=3.6.3737" },
|
||||
{ type: "violentmonkey", versionConstraint: ">=2.9.4" },
|
@@ -195,6 +195,7 @@ const compatMap = {
|
||||
exportValue: [],
|
||||
exportCookie: [],
|
||||
scriptUrl: [],
|
||||
storageName: [],
|
||||
},
|
||||
};
|
||||
|
@@ -126,6 +126,6 @@ const config = {
|
||||
};
|
||||
|
||||
// 以文本形式导出默认规则
|
||||
const defaultConfig = JSON.stringify(config);
|
||||
const defaultConfig = JSON.stringify(config, null, 2);
|
||||
|
||||
export { defaultConfig, userscriptsConfig, userscriptsRules };
|
8
packages/filesystem/README.md
Normal file
8
packages/filesystem/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 文件系统
|
||||
|
||||
用于同步和备份至云端
|
||||
|
||||
- zip
|
||||
- webdav
|
||||
- 百度网盘
|
||||
- onedrive
|
144
packages/filesystem/auth.ts
Normal file
144
packages/filesystem/auth.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { ExtServer, ExtServerApi } from "@App/app/const";
|
||||
import { WarpTokenError } from "./error";
|
||||
import { LocalStorageDAO } from "@App/app/repo/localStorage";
|
||||
|
||||
type NetDiskType = "baidu" | "onedrive";
|
||||
|
||||
export function GetNetDiskToken(netDiskType: NetDiskType): Promise<{
|
||||
code: number;
|
||||
msg: string;
|
||||
data: { token: { access_token: string; refresh_token: string } };
|
||||
}> {
|
||||
return fetch(ExtServerApi + `auth/net-disk/token?netDiskType=${netDiskType}`).then((resp) => resp.json());
|
||||
}
|
||||
|
||||
export function RefreshToken(
|
||||
netDiskType: NetDiskType,
|
||||
refreshToken: string
|
||||
): Promise<{
|
||||
code: number;
|
||||
msg: string;
|
||||
data: { token: { access_token: string; refresh_token: string } };
|
||||
}> {
|
||||
return fetch(ExtServerApi + `auth/net-disk/token/refresh?netDiskType=${netDiskType}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
netDiskType,
|
||||
refreshToken,
|
||||
}),
|
||||
}).then((resp) => resp.json());
|
||||
}
|
||||
|
||||
export function NetDisk(netDiskType: NetDiskType) {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (globalThis.window) {
|
||||
const loginWindow = window.open(`${ExtServer}api/v1/auth/net-disk?netDiskType=${netDiskType}`);
|
||||
const t = setInterval(() => {
|
||||
try {
|
||||
if (loginWindow!.closed) {
|
||||
clearInterval(t);
|
||||
resolve();
|
||||
}
|
||||
} catch (e) {
|
||||
clearInterval(t);
|
||||
resolve();
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
chrome.tabs
|
||||
.create({
|
||||
url: `${ExtServer}api/v1/auth/net-disk?netDiskType=${netDiskType}`,
|
||||
})
|
||||
.then(({ id: tabId }) => {
|
||||
const t = setInterval(async () => {
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId!);
|
||||
console.log("query tab", tab);
|
||||
if (!tab) {
|
||||
clearInterval(t);
|
||||
resolve();
|
||||
}
|
||||
} catch (e) {
|
||||
clearInterval(t);
|
||||
resolve();
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export type Token = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
createtime: number;
|
||||
};
|
||||
|
||||
export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) {
|
||||
let token: Token | undefined;
|
||||
const localStorageDao = new LocalStorageDAO();
|
||||
const key = `netdisk:token:${netDiskType}`;
|
||||
try {
|
||||
token = await localStorageDao.get(key).then((resp) => {
|
||||
if (resp) {
|
||||
return resp.value;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
// token不存在,或者没有accessToken,重新获取
|
||||
if (!token || !token.accessToken) {
|
||||
// 强制重新获取token
|
||||
await NetDisk(netDiskType);
|
||||
const resp = await GetNetDiskToken(netDiskType);
|
||||
if (resp.code !== 0) {
|
||||
return Promise.reject(new WarpTokenError(new Error(resp.msg)));
|
||||
}
|
||||
token = {
|
||||
accessToken: resp.data.token.access_token,
|
||||
refreshToken: resp.data.token.refresh_token,
|
||||
createtime: Date.now(),
|
||||
};
|
||||
invalid = false;
|
||||
await localStorageDao.save({
|
||||
key,
|
||||
value: token,
|
||||
});
|
||||
}
|
||||
// token过期或者失效
|
||||
if (Date.now() >= token.createtime + 3600000 || invalid) {
|
||||
// 大于一小时刷新token
|
||||
try {
|
||||
const resp = await RefreshToken(netDiskType, token.refreshToken);
|
||||
if (resp.code !== 0) {
|
||||
await localStorageDao.delete(key);
|
||||
// 刷新失败,并且标记失效,尝试重新获取token
|
||||
if (invalid) {
|
||||
return AuthVerify(netDiskType);
|
||||
}
|
||||
return Promise.reject(new WarpTokenError(new Error(resp.msg)));
|
||||
}
|
||||
token = {
|
||||
accessToken: resp.data.token.access_token,
|
||||
refreshToken: resp.data.token.refresh_token,
|
||||
createtime: Date.now(),
|
||||
};
|
||||
// 更新token
|
||||
await localStorageDao.save({
|
||||
key,
|
||||
value: token,
|
||||
});
|
||||
} catch (e) {
|
||||
// 报错返回原token
|
||||
return Promise.resolve(token.accessToken);
|
||||
}
|
||||
} else {
|
||||
return Promise.resolve(token.accessToken);
|
||||
}
|
||||
return Promise.resolve(token.accessToken);
|
||||
}
|
154
packages/filesystem/baidu/baidu.ts
Normal file
154
packages/filesystem/baidu/baidu.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { SystemConfig } from "@App/pkg/config/config";
|
||||
import { AuthVerify } from "../auth";
|
||||
import FileSystem, { File, FileReader, FileWriter } from "../filesystem";
|
||||
import { joinPath } from "../utils";
|
||||
import { BaiduFileReader, BaiduFileWriter } from "./rw";
|
||||
|
||||
export default class BaiduFileSystem implements FileSystem {
|
||||
accessToken?: string;
|
||||
|
||||
path: string;
|
||||
|
||||
constructor(path?: string, accessToken?: string) {
|
||||
this.path = path || "/apps";
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
async verify(): Promise<void> {
|
||||
const token = await AuthVerify("baidu");
|
||||
this.accessToken = token;
|
||||
return this.list().then();
|
||||
}
|
||||
|
||||
open(file: File): Promise<FileReader> {
|
||||
// 获取fsid
|
||||
return Promise.resolve(new BaiduFileReader(this, file));
|
||||
}
|
||||
|
||||
openDir(path: string): Promise<FileSystem> {
|
||||
return Promise.resolve(new BaiduFileSystem(joinPath(this.path, path), this.accessToken));
|
||||
}
|
||||
|
||||
create(path: string): Promise<FileWriter> {
|
||||
return Promise.resolve(new BaiduFileWriter(this, joinPath(this.path, path)));
|
||||
}
|
||||
|
||||
createDir(dir: string): Promise<void> {
|
||||
dir = joinPath(this.path, dir);
|
||||
const urlencoded = new URLSearchParams();
|
||||
urlencoded.append("path", dir);
|
||||
urlencoded.append("size", "0");
|
||||
urlencoded.append("isdir", "1");
|
||||
urlencoded.append("rtype", "3");
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
|
||||
return this.request(`https://pan.baidu.com/rest/2.0/xpan/file?method=create&access_token=${this.accessToken}`, {
|
||||
method: "POST",
|
||||
headers: myHeaders,
|
||||
body: urlencoded,
|
||||
redirect: "follow",
|
||||
}).then((data) => {
|
||||
if (data.errno) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
async request(url: string, config?: RequestInit) {
|
||||
config = config || {};
|
||||
const headers = <Headers>config.headers || new Headers();
|
||||
// 处理请求匿名不发送cookie
|
||||
await chrome.declarativeNetRequest.updateDynamicRules({
|
||||
removeRuleIds: [100],
|
||||
addRules: [
|
||||
{
|
||||
id: 100,
|
||||
action: {
|
||||
type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,
|
||||
responseHeaders: [{ operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE, header: "cookie" }],
|
||||
},
|
||||
condition: {
|
||||
urlFilter: url,
|
||||
resourceTypes: [chrome.declarativeNetRequest.ResourceType.XMLHTTPREQUEST],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
config.headers = headers;
|
||||
return fetch(url, config)
|
||||
.then((data) => data.json())
|
||||
.then(async (data) => {
|
||||
if (data.errno === 111 || data.errno === -6) {
|
||||
const token = await AuthVerify("baidu", true);
|
||||
this.accessToken = token;
|
||||
url = url.replace(/access_token=[^&]+/, `access_token=${token}`);
|
||||
return fetch(url, config)
|
||||
.then((data2) => data2.json())
|
||||
.then((data2) => {
|
||||
if (data2.errno === 111 || data2.errno === -6) {
|
||||
throw new Error(JSON.stringify(data2));
|
||||
}
|
||||
return data2;
|
||||
});
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.finally(() => {
|
||||
chrome.declarativeNetRequest.updateDynamicRules({
|
||||
removeRuleIds: [100],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
delete(path: string): Promise<void> {
|
||||
const filelist = [joinPath(this.path, path)];
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
|
||||
return this.request(
|
||||
`https://pan.baidu.com/rest/2.0/xpan/file?method=filemanager&access_token=${this.accessToken}&opera=delete`,
|
||||
{
|
||||
method: "POST",
|
||||
body: `async=0&filelist=${encodeURIComponent(JSON.stringify(filelist))}`,
|
||||
headers: myHeaders,
|
||||
}
|
||||
).then((data) => {
|
||||
if (data.errno) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
list(): Promise<File[]> {
|
||||
return this.request(
|
||||
`https://pan.baidu.com/rest/2.0/xpan/file?method=list&dir=${encodeURIComponent(
|
||||
this.path
|
||||
)}&order=time&access_token=${this.accessToken}`
|
||||
).then((data) => {
|
||||
if (data.errno) {
|
||||
if (data.errno === -9) {
|
||||
return [];
|
||||
}
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
const list: File[] = [];
|
||||
data.list.forEach((val: any) => {
|
||||
list.push({
|
||||
fsid: val.fs_id,
|
||||
name: val.server_filename,
|
||||
path: this.path,
|
||||
size: val.size,
|
||||
digest: val.md5,
|
||||
createtime: val.server_ctime * 1000,
|
||||
updatetime: val.server_mtime * 1000,
|
||||
});
|
||||
});
|
||||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
getDirUrl(): Promise<string> {
|
||||
return Promise.resolve(`https://pan.baidu.com/disk/main#/index?category=all&path=${encodeURIComponent(this.path)}`);
|
||||
}
|
||||
}
|
142
packages/filesystem/baidu/rw.ts
Normal file
142
packages/filesystem/baidu/rw.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { calculateMd5 } from "@App/pkg/utils/utils";
|
||||
import { MD5 } from "crypto-js";
|
||||
import { File, FileReader, FileWriter } from "../filesystem";
|
||||
import BaiduFileSystem from "./baidu";
|
||||
|
||||
export class BaiduFileReader implements FileReader {
|
||||
file: File;
|
||||
|
||||
fs: BaiduFileSystem;
|
||||
|
||||
constructor(fs: BaiduFileSystem, file: File) {
|
||||
this.fs = fs;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
async read(type?: "string" | "blob"): Promise<string | Blob> {
|
||||
// 查询文件信息获取dlink
|
||||
const data = await this.fs.request(
|
||||
`https://pan.baidu.com/rest/2.0/xpan/multimedia?method=filemetas&access_token=${
|
||||
this.fs.accessToken
|
||||
}&fsids=[${this.file.fsid!}]&dlink=1`
|
||||
);
|
||||
if (!data.list.length) {
|
||||
return Promise.reject(new Error("file not found"));
|
||||
}
|
||||
switch (type) {
|
||||
case "string":
|
||||
return fetch(
|
||||
`${data.list[0].dlink}&access_token=${this.fs.accessToken}`
|
||||
).then((resp) => resp.text());
|
||||
default: {
|
||||
return fetch(
|
||||
`${data.list[0].dlink}&access_token=${this.fs.accessToken}`
|
||||
).then((resp) => resp.blob());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BaiduFileWriter implements FileWriter {
|
||||
path: string;
|
||||
|
||||
fs: BaiduFileSystem;
|
||||
|
||||
constructor(fs: BaiduFileSystem, path: string) {
|
||||
this.fs = fs;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
size(content: string | Blob) {
|
||||
if (content instanceof Blob) {
|
||||
return content.size;
|
||||
}
|
||||
return new Blob([content]).size;
|
||||
}
|
||||
|
||||
async md5(content: string | Blob) {
|
||||
if (content instanceof Blob) {
|
||||
return calculateMd5(content);
|
||||
}
|
||||
return MD5(content).toString();
|
||||
}
|
||||
|
||||
async write(content: string | Blob): Promise<void> {
|
||||
// 预上传获取id
|
||||
const size = this.size(content).toString();
|
||||
const md5 = await this.md5(content);
|
||||
const blockList: string[] = [md5];
|
||||
let urlencoded = new URLSearchParams();
|
||||
urlencoded.append("path", this.path);
|
||||
urlencoded.append("size", size);
|
||||
urlencoded.append("isdir", "0");
|
||||
urlencoded.append("autoinit", "1");
|
||||
urlencoded.append("rtype", "3");
|
||||
urlencoded.append("block_list", JSON.stringify(blockList));
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
|
||||
const uploadid = await this.fs
|
||||
.request(
|
||||
`http://pan.baidu.com/rest/2.0/xpan/file?method=precreate&access_token=${this.fs.accessToken}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: myHeaders,
|
||||
body: urlencoded,
|
||||
}
|
||||
)
|
||||
.then((data) => {
|
||||
if (data.errno) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return data.uploadid;
|
||||
});
|
||||
const body = new FormData();
|
||||
if (content instanceof Blob) {
|
||||
// 分片上传
|
||||
body.append("file", content);
|
||||
} else {
|
||||
body.append("file", new Blob([content]));
|
||||
}
|
||||
|
||||
await this.fs
|
||||
.request(
|
||||
`${
|
||||
`https://d.pcs.baidu.com/rest/2.0/pcs/superfile2?method=upload&access_token=${this.fs.accessToken}` +
|
||||
`&type=tmpfile&path=`
|
||||
}${encodeURIComponent(this.path)}&uploadid=${uploadid}&partseq=0`,
|
||||
{
|
||||
method: "POST",
|
||||
body,
|
||||
}
|
||||
)
|
||||
.then((data) => {
|
||||
if (data.errno) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return data;
|
||||
});
|
||||
// 创建文件
|
||||
urlencoded = new URLSearchParams();
|
||||
urlencoded.append("path", this.path);
|
||||
urlencoded.append("size", size);
|
||||
urlencoded.append("isdir", "0");
|
||||
urlencoded.append("block_list", JSON.stringify(blockList));
|
||||
urlencoded.append("uploadid", uploadid);
|
||||
urlencoded.append("rtype", "3");
|
||||
return this.fs
|
||||
.request(
|
||||
`https://pan.baidu.com/rest/2.0/xpan/file?method=create&access_token=${this.fs.accessToken}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: myHeaders,
|
||||
body: urlencoded,
|
||||
}
|
||||
)
|
||||
.then((data) => {
|
||||
if (data.errno) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
}
|
23
packages/filesystem/error.ts
Normal file
23
packages/filesystem/error.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export class WarpTokenError {
|
||||
error: Error;
|
||||
|
||||
constructor(error: Error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
export function isWarpTokenError(error: any): error is WarpTokenError {
|
||||
return error instanceof WarpTokenError;
|
||||
}
|
||||
|
||||
export class WarpNetworkError {
|
||||
error: Error;
|
||||
|
||||
constructor(error: Error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
export function isNetworkError(error: any): error is WarpNetworkError {
|
||||
return error instanceof WarpNetworkError;
|
||||
}
|
84
packages/filesystem/factory.ts
Normal file
84
packages/filesystem/factory.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import i18next from "i18next";
|
||||
import BaiduFileSystem from "./baidu/baidu";
|
||||
import FileSystem from "./filesystem";
|
||||
import OneDriveFileSystem from "./onedrive/onedrive";
|
||||
import WebDAVFileSystem from "./webdav/webdav";
|
||||
import ZipFileSystem from "./zip/zip";
|
||||
|
||||
export type FileSystemType = "zip" | "webdav" | "baidu-netdsik" | "onedrive";
|
||||
|
||||
export type FileSystemParams = {
|
||||
[key: string]: {
|
||||
title: string;
|
||||
type?: "select" | "authorize" | "password";
|
||||
options?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export default class FileSystemFactory {
|
||||
static create(type: FileSystemType, params: any): Promise<FileSystem> {
|
||||
let fs: FileSystem;
|
||||
switch (type) {
|
||||
case "zip":
|
||||
fs = new ZipFileSystem(params);
|
||||
break;
|
||||
case "webdav":
|
||||
fs = new WebDAVFileSystem(
|
||||
params.authType,
|
||||
params.url,
|
||||
params.username,
|
||||
params.password
|
||||
);
|
||||
break;
|
||||
case "baidu-netdsik":
|
||||
fs = new BaiduFileSystem();
|
||||
break;
|
||||
case "onedrive":
|
||||
fs = new OneDriveFileSystem();
|
||||
break;
|
||||
default:
|
||||
throw new Error("not found filesystem");
|
||||
}
|
||||
return fs.verify().then(() => fs);
|
||||
}
|
||||
|
||||
static params(): { [key: string]: FileSystemParams } {
|
||||
return {
|
||||
webdav: {
|
||||
authType: {
|
||||
title: i18next.t("auth_type"),
|
||||
type: "select",
|
||||
options: ["password", "digest", "none", "token"],
|
||||
},
|
||||
url: { title: i18next.t("url") },
|
||||
username: { title: i18next.t("username") },
|
||||
password: { title: i18next.t("password"), type: "password" },
|
||||
},
|
||||
"baidu-netdsik": {},
|
||||
onedrive: {},
|
||||
};
|
||||
}
|
||||
|
||||
static async mkdirAll(fs: FileSystem, path: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const dirs = path.split("/");
|
||||
let i = 0;
|
||||
const mkdir = () => {
|
||||
if (i >= dirs.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const dir = dirs.slice(0, i + 1).join("/");
|
||||
fs.createDir(dir)
|
||||
.then(() => {
|
||||
i += 1;
|
||||
mkdir();
|
||||
})
|
||||
.catch(() => {
|
||||
reject();
|
||||
});
|
||||
};
|
||||
mkdir();
|
||||
});
|
||||
}
|
||||
}
|
48
packages/filesystem/filesystem.ts
Normal file
48
packages/filesystem/filesystem.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export interface File {
|
||||
fsid?: number;
|
||||
// 文件名
|
||||
name: string;
|
||||
// 文件路径
|
||||
path: string;
|
||||
// 文件大小
|
||||
size: number;
|
||||
// 文件摘要
|
||||
digest: string;
|
||||
// 文件创建时间
|
||||
createtime: number;
|
||||
// 文件修改时间
|
||||
updatetime: number;
|
||||
}
|
||||
|
||||
type ReadType = "string" | "blob";
|
||||
export interface FileReader {
|
||||
// 读取文件内容
|
||||
read(type?: ReadType): Promise<any>;
|
||||
}
|
||||
|
||||
export interface FileWriter {
|
||||
// 写入文件内容
|
||||
write(content: string | Blob): Promise<void>;
|
||||
}
|
||||
|
||||
export type FileReadWriter = FileReader & FileWriter;
|
||||
|
||||
// 文件读取
|
||||
export default interface FileSystem {
|
||||
// 授权验证
|
||||
verify(): Promise<void>;
|
||||
// 打开文件
|
||||
open(file: File): Promise<FileReader>;
|
||||
// 打开目录
|
||||
openDir(path: string): Promise<FileSystem>;
|
||||
// 创建文件
|
||||
create(path: string): Promise<FileWriter>;
|
||||
// 创建目录
|
||||
createDir(dir: string): Promise<void>;
|
||||
// 删除文件
|
||||
delete(path: string): Promise<void>;
|
||||
// 文件列表
|
||||
list(): Promise<File[]>;
|
||||
// getDirUrl 获取目录的url
|
||||
getDirUrl(): Promise<string>;
|
||||
}
|
149
packages/filesystem/onedrive/onedrive.ts
Normal file
149
packages/filesystem/onedrive/onedrive.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { AuthVerify } from "../auth";
|
||||
import FileSystem, { File, FileReader, FileWriter } from "../filesystem";
|
||||
import { joinPath } from "../utils";
|
||||
import { OneDriveFileReader, OneDriveFileWriter } from "./rw";
|
||||
|
||||
export default class OneDriveFileSystem implements FileSystem {
|
||||
accessToken?: string;
|
||||
|
||||
path: string;
|
||||
|
||||
constructor(path?: string, accessToken?: string) {
|
||||
this.path = path || "/";
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
async verify(): Promise<void> {
|
||||
const token = await AuthVerify("onedrive");
|
||||
this.accessToken = token;
|
||||
return this.list().then();
|
||||
}
|
||||
|
||||
open(file: File): Promise<FileReader> {
|
||||
return Promise.resolve(new OneDriveFileReader(this, file));
|
||||
}
|
||||
|
||||
openDir(path: string): Promise<FileSystem> {
|
||||
if (path.startsWith("ScriptCat")) {
|
||||
path = path.substring(9);
|
||||
}
|
||||
return Promise.resolve(new OneDriveFileSystem(joinPath(this.path, path), this.accessToken));
|
||||
}
|
||||
|
||||
create(path: string): Promise<FileWriter> {
|
||||
return Promise.resolve(new OneDriveFileWriter(this, joinPath(this.path, path)));
|
||||
}
|
||||
|
||||
createDir(dir: string): Promise<void> {
|
||||
if (dir && dir.startsWith("ScriptCat")) {
|
||||
dir = dir.substring(9);
|
||||
if (dir.startsWith("/")) {
|
||||
dir = dir.substring(1);
|
||||
}
|
||||
}
|
||||
if (!dir) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
dir = joinPath(this.path, dir);
|
||||
const dirs = dir.split("/");
|
||||
let parent = "";
|
||||
if (dirs.length > 2) {
|
||||
parent = dirs.slice(0, dirs.length - 1).join("/");
|
||||
}
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/json");
|
||||
if (parent !== "") {
|
||||
parent = `:${parent}:`;
|
||||
}
|
||||
return this.request(`https://graph.microsoft.com/v1.0/me/drive/special/approot${parent}/children`, {
|
||||
method: "POST",
|
||||
headers: myHeaders,
|
||||
body: JSON.stringify({
|
||||
name: dirs[dirs.length - 1],
|
||||
folder: {},
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
}),
|
||||
}).then((data: any) => {
|
||||
if (data.errno) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
request(url: string, config?: RequestInit, nothen?: boolean) {
|
||||
config = config || {};
|
||||
const headers = <Headers>config.headers || new Headers();
|
||||
if (url.indexOf("uploadSession") === -1) {
|
||||
headers.append(`Authorization`, `Bearer ${this.accessToken}`);
|
||||
}
|
||||
config.headers = headers;
|
||||
const ret = fetch(url, config);
|
||||
if (nothen) {
|
||||
return <Promise<Response>>ret;
|
||||
}
|
||||
return ret
|
||||
.then((data) => data.json())
|
||||
.then(async (data) => {
|
||||
if (data.error) {
|
||||
if (data.error.code === "InvalidAuthenticationToken") {
|
||||
const token = await AuthVerify("onedrive", true);
|
||||
this.accessToken = token;
|
||||
headers.set(`Authorization`, `Bearer ${this.accessToken}`);
|
||||
return fetch(url, config)
|
||||
.then((retryData) => retryData.json())
|
||||
.then((retryData) => {
|
||||
if (retryData.error) {
|
||||
throw new Error(JSON.stringify(retryData));
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
delete(path: string): Promise<void> {
|
||||
return this.request(
|
||||
`https://graph.microsoft.com/v1.0/me/drive/special/approot:${joinPath(this.path, path)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
true
|
||||
).then(async (resp) => {
|
||||
if (resp.status !== 204) {
|
||||
throw new Error(await resp.text());
|
||||
}
|
||||
return resp;
|
||||
});
|
||||
}
|
||||
|
||||
list(): Promise<File[]> {
|
||||
let { path } = this;
|
||||
if (path === "/") {
|
||||
path = "";
|
||||
} else {
|
||||
path = `:${path}:`;
|
||||
}
|
||||
return this.request(`https://graph.microsoft.com/v1.0/me/drive/special/approot${path}/children`).then((data) => {
|
||||
const list: File[] = [];
|
||||
data.value.forEach((val: any) => {
|
||||
list.push({
|
||||
name: val.name,
|
||||
path: this.path,
|
||||
size: val.size,
|
||||
digest: val.eTag,
|
||||
createtime: new Date(val.createdDateTime).getTime(),
|
||||
updatetime: new Date(val.lastModifiedDateTime).getTime(),
|
||||
});
|
||||
});
|
||||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
getDirUrl(): Promise<string> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
105
packages/filesystem/onedrive/rw.ts
Normal file
105
packages/filesystem/onedrive/rw.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { calculateMd5 } from "@App/pkg/utils/utils";
|
||||
import { MD5 } from "crypto-js";
|
||||
import { File, FileReader, FileWriter } from "../filesystem";
|
||||
import { joinPath } from "../utils";
|
||||
import OneDriveFileSystem from "./onedrive";
|
||||
|
||||
export class OneDriveFileReader implements FileReader {
|
||||
file: File;
|
||||
|
||||
fs: OneDriveFileSystem;
|
||||
|
||||
constructor(fs: OneDriveFileSystem, file: File) {
|
||||
this.fs = fs;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
async read(type?: "string" | "blob"): Promise<string | Blob> {
|
||||
const data = await this.fs.request(
|
||||
`https://graph.microsoft.com/v1.0/me/drive/special/approot:${joinPath(
|
||||
this.file.path,
|
||||
this.file.name
|
||||
)}:/content`,
|
||||
{},
|
||||
true
|
||||
);
|
||||
if (data.status !== 200) {
|
||||
return Promise.reject(await data.text());
|
||||
}
|
||||
switch (type) {
|
||||
case "string":
|
||||
return data.text();
|
||||
default: {
|
||||
return data.blob();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OneDriveFileWriter implements FileWriter {
|
||||
path: string;
|
||||
|
||||
fs: OneDriveFileSystem;
|
||||
|
||||
constructor(fs: OneDriveFileSystem, path: string) {
|
||||
this.fs = fs;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
size(content: string | Blob) {
|
||||
if (content instanceof Blob) {
|
||||
return content.size;
|
||||
}
|
||||
return new Blob([content]).size;
|
||||
}
|
||||
|
||||
async md5(content: string | Blob) {
|
||||
if (content instanceof Blob) {
|
||||
return calculateMd5(content);
|
||||
}
|
||||
return MD5(content).toString();
|
||||
}
|
||||
|
||||
async write(content: string | Blob): Promise<void> {
|
||||
// 预上传获取id
|
||||
const size = this.size(content).toString();
|
||||
let myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/json");
|
||||
const uploadUrl = await this.fs
|
||||
.request(
|
||||
`https://graph.microsoft.com/v1.0/me/drive/special/approot:${this.path}:/createUploadSession`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: myHeaders,
|
||||
body: JSON.stringify({
|
||||
item: {
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
// description: "description",
|
||||
// fileSystemInfo: {
|
||||
// "@odata.type": "microsoft.graph.fileSystemInfo",
|
||||
// },
|
||||
// name: this.path.substring(this.path.lastIndexOf("/") + 1),
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return data.uploadUrl;
|
||||
});
|
||||
myHeaders = new Headers();
|
||||
myHeaders.append(
|
||||
"Content-Range",
|
||||
`bytes 0-${parseInt(size, 10) - 1}/${size}`
|
||||
);
|
||||
return this.fs.request(uploadUrl, {
|
||||
method: "PUT",
|
||||
body: content,
|
||||
headers: myHeaders,
|
||||
});
|
||||
}
|
||||
}
|
18
packages/filesystem/utils.ts
Normal file
18
packages/filesystem/utils.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
export function joinPath(...paths: string[]): string {
|
||||
let path = "";
|
||||
paths.forEach((value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (!value.startsWith("/")) {
|
||||
value = `/${value}`;
|
||||
}
|
||||
if (value.endsWith("/")) {
|
||||
value = value.substring(0, value.length - 1);
|
||||
}
|
||||
path += value;
|
||||
});
|
||||
return path;
|
||||
}
|
52
packages/filesystem/webdav/rw.ts
Normal file
52
packages/filesystem/webdav/rw.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { WebDAVClient } from "webdav";
|
||||
import { FileReader, FileWriter } from "../filesystem";
|
||||
|
||||
export class WebDAVFileReader implements FileReader {
|
||||
client: WebDAVClient;
|
||||
|
||||
path: string;
|
||||
|
||||
constructor(client: WebDAVClient, path: string) {
|
||||
this.client = client;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
async read(type?: "string" | "blob"): Promise<string | Blob> {
|
||||
switch (type) {
|
||||
case "string":
|
||||
return this.client.getFileContents(this.path, {
|
||||
format: "text",
|
||||
}) as Promise<string>;
|
||||
default: {
|
||||
const resp = (await this.client.getFileContents(this.path, {
|
||||
format: "binary",
|
||||
})) as ArrayBuffer;
|
||||
return Promise.resolve(new Blob([resp]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class WebDAVFileWriter implements FileWriter {
|
||||
client: WebDAVClient;
|
||||
|
||||
path: string;
|
||||
|
||||
constructor(client: WebDAVClient, path: string) {
|
||||
this.client = client;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
async write(content: string | Blob): Promise<void> {
|
||||
let resp;
|
||||
if (content instanceof Blob) {
|
||||
resp = await this.client.putFileContents(this.path, await content.arrayBuffer());
|
||||
} else {
|
||||
resp = await this.client.putFileContents(this.path, content);
|
||||
}
|
||||
if (resp) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error("write error"));
|
||||
}
|
||||
}
|
91
packages/filesystem/webdav/webdav.ts
Normal file
91
packages/filesystem/webdav/webdav.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { AuthType, createClient, FileStat, WebDAVClient } from "webdav";
|
||||
import FileSystem, { File, FileReader, FileWriter } from "../filesystem";
|
||||
import { joinPath } from "../utils";
|
||||
import { WebDAVFileReader, WebDAVFileWriter } from "./rw";
|
||||
import { WarpTokenError } from "../error";
|
||||
|
||||
export default class WebDAVFileSystem implements FileSystem {
|
||||
client: WebDAVClient;
|
||||
|
||||
url: string;
|
||||
|
||||
basePath: string = "/";
|
||||
|
||||
constructor(authType: AuthType | WebDAVClient, url?: string, username?: string, password?: string) {
|
||||
if (typeof authType === "object") {
|
||||
this.client = authType;
|
||||
this.basePath = joinPath(url || "");
|
||||
this.url = username!;
|
||||
} else {
|
||||
this.url = url!;
|
||||
this.client = createClient(url!, {
|
||||
authType,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async verify(): Promise<void> {
|
||||
try {
|
||||
await this.client.getQuota();
|
||||
} catch (e: any) {
|
||||
if (e.response && e.response.status === 401) {
|
||||
throw new WarpTokenError(e);
|
||||
}
|
||||
throw new Error("verify failed");
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
open(file: File): Promise<FileReader> {
|
||||
return Promise.resolve(new WebDAVFileReader(this.client, joinPath(file.path, file.name)));
|
||||
}
|
||||
|
||||
openDir(path: string): Promise<FileSystem> {
|
||||
return Promise.resolve(new WebDAVFileSystem(this.client, joinPath(this.basePath, path), this.url));
|
||||
}
|
||||
|
||||
create(path: string): Promise<FileWriter> {
|
||||
return Promise.resolve(new WebDAVFileWriter(this.client, joinPath(this.basePath, path)));
|
||||
}
|
||||
|
||||
async createDir(path: string): Promise<void> {
|
||||
try {
|
||||
return Promise.resolve(await this.client.createDirectory(joinPath(this.basePath, path)));
|
||||
} catch (e: any) {
|
||||
// 如果是405错误,则忽略
|
||||
if (e.message.includes("405")) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(path: string): Promise<void> {
|
||||
return this.client.deleteFile(joinPath(this.basePath, path));
|
||||
}
|
||||
|
||||
async list(): Promise<File[]> {
|
||||
const dir = (await this.client.getDirectoryContents(this.basePath)) as FileStat[];
|
||||
const ret: File[] = [];
|
||||
dir.forEach((item: FileStat) => {
|
||||
if (item.type !== "file") {
|
||||
return;
|
||||
}
|
||||
ret.push({
|
||||
name: item.basename,
|
||||
path: this.basePath,
|
||||
digest: item.etag || "",
|
||||
size: item.size,
|
||||
createtime: new Date(item.lastmod).getTime(),
|
||||
updatetime: new Date(item.lastmod).getTime(),
|
||||
});
|
||||
});
|
||||
return Promise.resolve(ret);
|
||||
}
|
||||
|
||||
getDirUrl(): Promise<string> {
|
||||
return Promise.resolve(this.url + this.basePath);
|
||||
}
|
||||
}
|
32
packages/filesystem/zip/rw.ts
Normal file
32
packages/filesystem/zip/rw.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import JSZip, { JSZipObject } from "jszip";
|
||||
import { FileReader, FileWriter } from "../filesystem";
|
||||
|
||||
export class ZipFileReader implements FileReader {
|
||||
zipObject: JSZipObject;
|
||||
|
||||
constructor(zipObject: JSZipObject) {
|
||||
this.zipObject = zipObject;
|
||||
}
|
||||
|
||||
read(type?: "string" | "blob"): Promise<string | Blob> {
|
||||
return this.zipObject.async(type || "string");
|
||||
}
|
||||
}
|
||||
|
||||
export class ZipFileWriter implements FileWriter {
|
||||
zip: JSZip;
|
||||
|
||||
path: string;
|
||||
|
||||
constructor(zip: JSZip, path: string) {
|
||||
this.zip = zip;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
write(content: string): Promise<void> {
|
||||
this.zip.file(this.path, content);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
68
packages/filesystem/zip/zip.ts
Normal file
68
packages/filesystem/zip/zip.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import JSZip from "jszip";
|
||||
import FileSystem, {
|
||||
File,
|
||||
FileReader,
|
||||
FileWriter,
|
||||
} from "@Pkg/filesystem/filesystem";
|
||||
import { ZipFileReader, ZipFileWriter } from "./rw";
|
||||
|
||||
export default class ZipFileSystem implements FileSystem {
|
||||
zip: JSZip;
|
||||
|
||||
basePath: string;
|
||||
|
||||
// zip为空时,创建一个空的zip
|
||||
constructor(zip?: JSZip, basePath?: string) {
|
||||
this.zip = zip || new JSZip();
|
||||
this.basePath = basePath || "";
|
||||
}
|
||||
|
||||
verify(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
open(info: File): Promise<FileReader> {
|
||||
const path = info.name;
|
||||
const file = this.zip.file(path);
|
||||
if (file) {
|
||||
return Promise.resolve(new ZipFileReader(file));
|
||||
}
|
||||
return Promise.reject(new Error("File not found"));
|
||||
}
|
||||
|
||||
openDir(path: string): Promise<FileSystem> {
|
||||
return Promise.resolve(new ZipFileSystem(this.zip, path));
|
||||
}
|
||||
|
||||
create(path: string): Promise<FileWriter> {
|
||||
return Promise.resolve(new ZipFileWriter(this.zip, path));
|
||||
}
|
||||
|
||||
createDir(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
delete(path: string): Promise<void> {
|
||||
this.zip.remove(path);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
list(): Promise<File[]> {
|
||||
const files: File[] = [];
|
||||
Object.keys(this.zip.files).forEach((key) => {
|
||||
files.push({
|
||||
name: key,
|
||||
path: key,
|
||||
size: 0,
|
||||
digest: "",
|
||||
createtime: this.zip.files[key].date.getTime(),
|
||||
updatetime: this.zip.files[key].date.getTime(),
|
||||
});
|
||||
});
|
||||
return Promise.resolve(files);
|
||||
}
|
||||
|
||||
getDirUrl(): Promise<string> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,7 @@ export class CustomEventMessage implements Message {
|
||||
EE: EventEmitter = new EventEmitter();
|
||||
|
||||
// 关联dom目标
|
||||
relatedTarget: Map<number, Document> = new Map();
|
||||
relatedTarget: Map<number, EventTarget> = new Map();
|
||||
|
||||
constructor(
|
||||
protected flag: string,
|
||||
@@ -25,7 +25,7 @@ export class CustomEventMessage implements Message {
|
||||
) {
|
||||
window.addEventListener((isContent ? "ct" : "fd") + flag, (event) => {
|
||||
if (event instanceof MouseEvent) {
|
||||
this.relatedTarget.set(event.clientX, <Document>event.relatedTarget);
|
||||
this.relatedTarget.set(event.clientX, event.relatedTarget!);
|
||||
return;
|
||||
} else if (event instanceof CustomEvent) {
|
||||
this.messageHandle(event.detail, new CustomEventPostMessage(this));
|
||||
@@ -82,23 +82,7 @@ export class CustomEventMessage implements Message {
|
||||
});
|
||||
}
|
||||
|
||||
nativeSend(data: any) {
|
||||
let detail = data;
|
||||
|
||||
// 特殊处理relatedTarget
|
||||
if (detail.data && typeof detail.data.relatedTarget === "object") {
|
||||
// 先将relatedTarget转换成id发送过去
|
||||
const target = detail.data.relatedTarget;
|
||||
delete detail.data.relatedTarget;
|
||||
detail.data.relatedTarget = Math.ceil(Math.random() * 1000000);
|
||||
// 可以使用此种方式交互element
|
||||
const ev = new MouseEvent((this.isContent ? "fd" : "ct") + this.flag, {
|
||||
clientX: detail.data.relatedTarget,
|
||||
relatedTarget: target,
|
||||
});
|
||||
window.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
nativeSend(detail: any) {
|
||||
if (typeof cloneInto !== "undefined") {
|
||||
try {
|
||||
LoggerCore.logger().info("nativeSend");
|
||||
@@ -131,6 +115,40 @@ export class CustomEventMessage implements Message {
|
||||
});
|
||||
}
|
||||
|
||||
// 同步发送消息
|
||||
// 与content页的消息通讯实际是同步,此方法不需要经过background
|
||||
// 但是请注意中间不要有promise
|
||||
syncSendMessage(data: any): any {
|
||||
const body: WindowMessageBody = {
|
||||
messageId: uuidv4(),
|
||||
type: "sendMessage",
|
||||
data,
|
||||
};
|
||||
let ret: any;
|
||||
const callback = (body: WindowMessageBody) => {
|
||||
this.EE.removeListener("response:" + body.messageId, callback);
|
||||
ret = body.data;
|
||||
};
|
||||
this.EE.addListener("response:" + body.messageId, callback);
|
||||
this.nativeSend(body);
|
||||
return ret;
|
||||
}
|
||||
|
||||
relateId = 0;
|
||||
|
||||
sendRelatedTarget(target: EventTarget): number {
|
||||
// 特殊处理relatedTarget,返回id进行关联
|
||||
// 先将relatedTarget转换成id发送过去
|
||||
const id = ++this.relateId;
|
||||
// 可以使用此种方式交互element
|
||||
const ev = new MouseEvent((this.isContent ? "fd" : "ct") + this.flag, {
|
||||
clientX: id,
|
||||
relatedTarget: target,
|
||||
});
|
||||
window.dispatchEvent(ev);
|
||||
return id;
|
||||
}
|
||||
|
||||
getAndDelRelatedTarget(id: number) {
|
||||
const target = this.relatedTarget.get(id);
|
||||
this.relatedTarget.delete(id);
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import { Message, MessageConnect, MessageSend, MessageSender } from "./server";
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
|
||||
export class ExtensionMessageSend implements MessageSend {
|
||||
constructor() {}
|
||||
@@ -44,6 +46,10 @@ export class ServiceWorkerMessage extends ExtensionMessageSend implements Messag
|
||||
}
|
||||
|
||||
export class ExtensionMessage extends ExtensionMessageSend implements Message {
|
||||
constructor(private onUserScript = false) {
|
||||
super();
|
||||
}
|
||||
|
||||
onConnect(callback: (data: any, con: MessageConnect) => void) {
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
const handler = (msg: any) => {
|
||||
@@ -52,6 +58,16 @@ export class ExtensionMessage extends ExtensionMessageSend implements Message {
|
||||
};
|
||||
port.onMessage.addListener(handler);
|
||||
});
|
||||
if (this.onUserScript) {
|
||||
// 监听用户脚本的连接
|
||||
chrome.runtime.onUserScriptConnect.addListener((port) => {
|
||||
const handler = (msg: any) => {
|
||||
port.onMessage.removeListener(handler);
|
||||
callback(msg, new ExtensionMessageConnect(port));
|
||||
};
|
||||
port.onMessage.addListener(handler);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 注意chrome.runtime.onMessage.addListener的回调函数需要返回true才能处理异步请求
|
||||
@@ -62,6 +78,15 @@ export class ExtensionMessage extends ExtensionMessageSend implements Message {
|
||||
}
|
||||
return callback(msg, sendResponse, sender);
|
||||
});
|
||||
if (this.onUserScript) {
|
||||
// 监听用户脚本的消息
|
||||
chrome.runtime.onUserScriptMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (msg.action === "messageQueue") {
|
||||
return false;
|
||||
}
|
||||
return callback(msg, sendResponse, sender);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import { connect } from "./client";
|
||||
import { connect, sendMessage } from "./client";
|
||||
|
||||
export interface Message extends MessageSend {
|
||||
onConnect(callback: (data: any, con: MessageConnect) => void): void;
|
||||
@@ -20,6 +20,12 @@ export interface MessageConnect {
|
||||
|
||||
export type MessageSender = chrome.runtime.MessageSender;
|
||||
|
||||
export type ExtMessageSender = {
|
||||
tabId: number;
|
||||
frameId?: number;
|
||||
documentId?: string;
|
||||
};
|
||||
|
||||
export class GetSender {
|
||||
constructor(private sender: MessageConnect | MessageSender) {}
|
||||
|
||||
@@ -27,12 +33,22 @@ export class GetSender {
|
||||
return this.sender as MessageSender;
|
||||
}
|
||||
|
||||
getExtMessageSender(): ExtMessageSender {
|
||||
const sender = this.sender as MessageSender;
|
||||
return {
|
||||
tabId: sender.tab?.id || -1, // -1表示后台脚本
|
||||
frameId: sender.frameId,
|
||||
documentId: sender.documentId,
|
||||
};
|
||||
}
|
||||
|
||||
getConnect(): MessageConnect {
|
||||
return this.sender as MessageConnect;
|
||||
}
|
||||
}
|
||||
|
||||
export type ApiFunction = (params: any, con: GetSender) => Promise<any> | void;
|
||||
export type ApiFunctionSync = (params: any, con: GetSender) => any;
|
||||
|
||||
export class Server {
|
||||
private apiFunctionMap: Map<string, ApiFunction> = new Map();
|
||||
@@ -72,8 +88,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 +105,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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,14 +131,14 @@ export class Group {
|
||||
}
|
||||
|
||||
// 转发消息
|
||||
export function forwardMessage(prefix: string, path: string, from: Server, to: MessageSend, middleware?: ApiFunction) {
|
||||
from.on(path, async (params, fromCon) => {
|
||||
if (middleware) {
|
||||
const resp = await middleware(params, fromCon);
|
||||
if (resp !== false) {
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
export function forwardMessage(
|
||||
prefix: string,
|
||||
path: string,
|
||||
from: Server,
|
||||
to: MessageSend,
|
||||
middleware?: ApiFunctionSync
|
||||
) {
|
||||
const handler = (params: any, fromCon: GetSender) => {
|
||||
const fromConnect = fromCon.getConnect();
|
||||
if (fromConnect) {
|
||||
connect(to, prefix + "/" + path, params).then((toCon) => {
|
||||
@@ -140,7 +156,24 @@ export function forwardMessage(prefix: string, path: string, from: Server, to: M
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return to.sendMessage({ action: prefix + "/" + path, data: params });
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
819
pnpm-lock.yaml
generated
819
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@ import { defineConfig } from "@rspack/cli";
|
||||
import { rspack } from "@rspack/core";
|
||||
import { version } from "./package.json";
|
||||
|
||||
const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
const isBeta = version.includes("-");
|
||||
|
||||
@@ -33,9 +35,11 @@ export default defineConfig({
|
||||
inject: `${src}/inject.ts`,
|
||||
popup: `${src}/pages/popup/main.tsx`,
|
||||
install: `${src}/pages/install/main.tsx`,
|
||||
confirm: `${src}/pages/confirm/main.tsx`,
|
||||
options: `${src}/pages/options/main.tsx`,
|
||||
"editor.worker": "monaco-editor/esm/vs/editor/editor.worker.js",
|
||||
"ts.worker": "monaco-editor/esm/vs/language/typescript/ts.worker.js",
|
||||
"linter.worker": `${src}/linter.worker.ts`,
|
||||
},
|
||||
output: {
|
||||
path: `${dist}/ext/src`,
|
||||
@@ -47,6 +51,9 @@ export default defineConfig({
|
||||
alias: {
|
||||
"@App": path.resolve(__dirname, "src/"),
|
||||
"@Packages": path.resolve(__dirname, "packages/"),
|
||||
// 改写eslint-plugin-userscripts以适配脚本猫,打包时重定义模块路径
|
||||
"../data/compat-grant": path.resolve(__dirname, "./packages/eslint/compat-grant"),
|
||||
"../data/compat-headers": path.resolve(__dirname, "./packages/eslint/compat-headers"),
|
||||
},
|
||||
fallback: {
|
||||
child_process: false,
|
||||
@@ -145,6 +152,15 @@ export default defineConfig({
|
||||
minify: true,
|
||||
chunks: ["install"],
|
||||
}),
|
||||
,
|
||||
new rspack.HtmlRspackPlugin({
|
||||
filename: `${dist}/ext/src/confirm.html`,
|
||||
template: `${src}/pages/template.html`,
|
||||
inject: "head",
|
||||
title: "Confirm - ScriptCat",
|
||||
minify: true,
|
||||
chunks: ["confirm"],
|
||||
}),
|
||||
new rspack.HtmlRspackPlugin({
|
||||
filename: `${dist}/ext/src/options.html`,
|
||||
template: `${src}/pages/options.html`,
|
||||
@@ -175,6 +191,7 @@ export default defineConfig({
|
||||
minify: true,
|
||||
chunks: ["sandbox"],
|
||||
}),
|
||||
new NodePolyfillPlugin(),
|
||||
].filter(Boolean),
|
||||
optimization: {
|
||||
minimizer: [
|
||||
|
@@ -24,8 +24,9 @@ if (version.prerelease.length) {
|
||||
throw new Error("未知的版本类型");
|
||||
}
|
||||
manifest.version = `${version.major.toString()}.${version.minor.toString()}.${version.patch.toString()}.${betaVersion.toString()}`;
|
||||
manifest.name = `${manifest.name} Beta`;
|
||||
manifest.name = `__MSG_scriptcat_beta__`;
|
||||
} else {
|
||||
manifest.name = `__MSG_scriptcat__`;
|
||||
manifest.version = package.version;
|
||||
}
|
||||
|
||||
|
@@ -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> {
|
||||
@@ -135,27 +135,49 @@ export default class Cache {
|
||||
return this.storage.list();
|
||||
}
|
||||
|
||||
private txPromise: Map<string, Promise<any>> = new Map();
|
||||
private txLock: Map<string, ((unlock: () => void) => void)[]> = new Map();
|
||||
|
||||
lock(key: string): Promise<() => void> | (() => void) {
|
||||
let hasLock = this.txLock.has(key);
|
||||
|
||||
const unlock = () => {
|
||||
let waitFunc = this.txLock.get(key)?.shift();
|
||||
if (waitFunc) {
|
||||
waitFunc(unlock);
|
||||
} else {
|
||||
this.txLock.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
if (hasLock) {
|
||||
let lock = this.txLock.get(key);
|
||||
if (!lock) {
|
||||
lock = [];
|
||||
this.txLock.set(key, lock);
|
||||
}
|
||||
return new Promise<() => void>((resolve) => {
|
||||
lock.push(resolve);
|
||||
});
|
||||
}
|
||||
this.txLock.set(key, []);
|
||||
return unlock;
|
||||
}
|
||||
|
||||
// 事务处理,如果有事务正在进行,则等待
|
||||
public async tx<T>(key: string, set: (result: T) => Promise<T>): Promise<T> {
|
||||
let promise = this.txPromise.get(key);
|
||||
if (promise) {
|
||||
await promise;
|
||||
}
|
||||
const unlock = await this.lock(key);
|
||||
let newValue: T;
|
||||
promise = this.get(key)
|
||||
await this.get(key)
|
||||
.then((result) => set(result))
|
||||
.then((value) => {
|
||||
if (value) {
|
||||
newValue = value;
|
||||
return this.set(key, value);
|
||||
} else if (value === undefined) {
|
||||
return this.del(key);
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
this.txPromise.set(key, promise);
|
||||
await promise;
|
||||
this.txPromise.delete(key);
|
||||
unlock();
|
||||
return newValue!;
|
||||
}
|
||||
}
|
||||
|
@@ -3,12 +3,8 @@ import { version } from "../../package.json";
|
||||
export const ExtVersion = version;
|
||||
|
||||
export const ExtServer = "https://ext.scriptcat.org/";
|
||||
export const ExtServerApi = ExtServer + "api/v1/";
|
||||
|
||||
export const ExternalWhitelist = [
|
||||
"greasyfork.org",
|
||||
"scriptcat.org",
|
||||
"tampermonkey.net.cn",
|
||||
"openuserjs.org",
|
||||
];
|
||||
export const ExternalWhitelist = ["greasyfork.org", "scriptcat.org", "tampermonkey.net.cn", "openuserjs.org"];
|
||||
|
||||
export const ExternalMessage = "externalMessage";
|
||||
|
@@ -9,13 +9,17 @@ export default class DBWriter implements Writer {
|
||||
this.dao = dao;
|
||||
}
|
||||
|
||||
write(level: LogLevel, message: string, label: LogLabel): void {
|
||||
this.dao.save({
|
||||
id: 0,
|
||||
level,
|
||||
message,
|
||||
label,
|
||||
createtime: new Date().getTime(),
|
||||
});
|
||||
async write(level: LogLevel, message: string, label: LogLabel): Promise<void> {
|
||||
try {
|
||||
await this.dao.save({
|
||||
id: 0,
|
||||
level,
|
||||
message,
|
||||
label,
|
||||
createtime: new Date().getTime(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("DBWriter error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -32,28 +32,27 @@ export default class Logger {
|
||||
}
|
||||
|
||||
log(level: LogLevel, message: string, ...label: LogLabel[]) {
|
||||
const newLabel = buildLabel(this.label, label);
|
||||
if (levelNumber[level] >= levelNumber[this.core.level]) {
|
||||
this.core.writer.write(level, message, buildLabel(this.label, label));
|
||||
this.core.writer.write(level, message, newLabel);
|
||||
}
|
||||
if (this.core.debug !== "none" && levelNumber[level] >= levelNumber[this.core.debug]) {
|
||||
if (typeof message === "object") {
|
||||
message = JSON.stringify(message);
|
||||
}
|
||||
const msg = `${dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")} [${level}] msg=${message} label=${JSON.stringify(
|
||||
buildLabel(this.label, label)
|
||||
)}`;
|
||||
const msg = `${dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")} [${level}] ${message}`;
|
||||
switch (level) {
|
||||
case "error":
|
||||
console.error(msg);
|
||||
console.error(msg, newLabel);
|
||||
break;
|
||||
case "warn":
|
||||
console.warn(msg);
|
||||
console.warn(msg, newLabel);
|
||||
break;
|
||||
case "trace":
|
||||
console.info(msg);
|
||||
console.info(msg, newLabel);
|
||||
break;
|
||||
default:
|
||||
console.info(msg);
|
||||
console.info(msg, newLabel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
17
src/app/repo/localStorage.ts
Normal file
17
src/app/repo/localStorage.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Repo } from "./repo";
|
||||
|
||||
export interface LocalStorageItem {
|
||||
key: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
// 由于service worker不能使用localStorage,这里新建一个类来实现localStorage的功能
|
||||
export class LocalStorageDAO extends Repo<LocalStorageItem> {
|
||||
constructor() {
|
||||
super("localStorage");
|
||||
}
|
||||
|
||||
save(value: LocalStorageItem) {
|
||||
return super._save(value.key, value);
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||
import { Client, sendMessage } from "@Packages/message/client";
|
||||
import { CustomEventMessage } from "@Packages/message/custom_event_message";
|
||||
import { forwardMessage, Message, MessageSend, Server } from "@Packages/message/server";
|
||||
|
||||
// content页的处理
|
||||
@@ -12,9 +13,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
|
||||
@@ -34,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());
|
||||
@@ -52,8 +53,40 @@ export default class ContentRuntime {
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
case "GM_addElement": {
|
||||
let [parentNodeId, tagName, attr] = data.params;
|
||||
let parentNode: EventTarget | undefined;
|
||||
if (parentNodeId) {
|
||||
parentNode = (this.msg as CustomEventMessage).getAndDelRelatedTarget(parentNodeId);
|
||||
}
|
||||
const el = <Element>document.createElement(tagName);
|
||||
|
||||
let textContent = "";
|
||||
if (attr) {
|
||||
if (attr.textContent) {
|
||||
textContent = attr.textContent;
|
||||
delete attr.textContent;
|
||||
}
|
||||
} else {
|
||||
attr = {};
|
||||
}
|
||||
Object.keys(attr).forEach((key) => {
|
||||
el.setAttribute(key, attr[key]);
|
||||
});
|
||||
if (textContent) {
|
||||
el.innerHTML = textContent;
|
||||
}
|
||||
(<Element>parentNode || document.head || document.body || document.querySelector("*")).appendChild(el);
|
||||
const nodeId = (this.msg as CustomEventMessage).sendRelatedTarget(el);
|
||||
return nodeId;
|
||||
}
|
||||
case "GM_log":
|
||||
// 拦截GM_log,打印到控制台
|
||||
// 由于某些页面会处理掉console.log,所以丢到这里来打印
|
||||
console.log(...data.params);
|
||||
break;
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
return false;
|
||||
}
|
||||
);
|
||||
const client = new Client(this.msg, "inject");
|
@@ -4,6 +4,7 @@ import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||
import GMApi from "./gm_api";
|
||||
import { compileScript, createContext, proxyContext, ScriptFunc } from "./utils";
|
||||
import { Message } from "@Packages/message/server";
|
||||
import { EmitEventRequest } from "../service_worker/runtime";
|
||||
|
||||
export type ValueUpdateSender = {
|
||||
runFlag: string;
|
||||
@@ -45,7 +46,7 @@ export default class ExecScript {
|
||||
this.scriptRes = scriptRes;
|
||||
this.logger = LoggerCore.getInstance().logger({
|
||||
component: "exec",
|
||||
script: this.scriptRes.uuid,
|
||||
uuid: this.scriptRes.uuid,
|
||||
name: this.scriptRes.name,
|
||||
});
|
||||
this.GM_info = GMApi.GM_info(this.scriptRes);
|
||||
@@ -69,13 +70,13 @@ export default class ExecScript {
|
||||
}
|
||||
}
|
||||
|
||||
// 触发值更新
|
||||
valueUpdate(data: ValueUpdateData) {
|
||||
this.sandboxContent?.valueUpdate(data);
|
||||
emitEvent(event: string, eventId: string, data: any) {
|
||||
this.logger.debug("emit event", { event, eventId, data });
|
||||
this.sandboxContent?.emitEvent(event, eventId, data);
|
||||
}
|
||||
|
||||
menuClick(id: number) {
|
||||
this.sandboxContent?.menuClick(id);
|
||||
valueUpdate(data: ValueUpdateData) {
|
||||
this.sandboxContent?.valueUpdate(data);
|
||||
}
|
||||
|
||||
exec() {
|
@@ -1,17 +1,16 @@
|
||||
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||
import { getMetadataStr, getUserConfigStr, parseUserConfig } from "@App/pkg/utils/script";
|
||||
import { base64ToBlob, 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[];
|
||||
listener?: () => void;
|
||||
}
|
||||
|
||||
export interface ApiValue {
|
||||
@@ -25,9 +24,6 @@ export class GMContext {
|
||||
public static API(param: ApiParam = {}) {
|
||||
return (target: any, propertyName: string, descriptor: PropertyDescriptor) => {
|
||||
const key = propertyName;
|
||||
if (param.listener) {
|
||||
param.listener();
|
||||
}
|
||||
if (key === "GMdotXmlHttpRequest") {
|
||||
GMContext.apis.set("GM.xmlHttpRequest", {
|
||||
api: descriptor.value,
|
||||
@@ -103,8 +99,8 @@ export default class GMApi {
|
||||
}
|
||||
}
|
||||
|
||||
menuClick(id: number) {
|
||||
this.EE.emit("menuClick" + id);
|
||||
emitEvent(event: string, eventId: string, data: any) {
|
||||
this.EE.emit(event + ":" + eventId, data);
|
||||
}
|
||||
|
||||
// 获取脚本信息和管理器信息
|
||||
@@ -174,17 +170,17 @@ export default class GMApi {
|
||||
this.GM_setValue(name, undefined);
|
||||
}
|
||||
|
||||
valueChangeId: number | undefined;
|
||||
eventId: number = 0;
|
||||
|
||||
menuMap: Map<number, string> | undefined;
|
||||
|
||||
EE: EventEmitter = new EventEmitter();
|
||||
|
||||
@GMContext.API()
|
||||
public GM_addValueChangeListener(name: string, listener: GMTypes.ValueChangeListener): number {
|
||||
if (!this.valueChangeId) {
|
||||
this.valueChangeId = 1;
|
||||
} else {
|
||||
this.valueChangeId += 1;
|
||||
}
|
||||
this.valueChangeListener.set(this.valueChangeId, { name, listener });
|
||||
return this.valueChangeId;
|
||||
this.eventId += 1;
|
||||
this.valueChangeListener.set(this.eventId, { name, listener });
|
||||
return this.eventId;
|
||||
}
|
||||
|
||||
@GMContext.API()
|
||||
@@ -219,14 +215,23 @@ 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;
|
||||
}
|
||||
|
||||
menuId: number | undefined;
|
||||
|
||||
menuMap: Map<number, string> | undefined;
|
||||
|
||||
EE: EventEmitter = new EventEmitter();
|
||||
@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()
|
||||
GM_registerMenuCommand(name: string, listener: () => void, accessKey?: string): number {
|
||||
@@ -240,31 +245,125 @@ 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.EE.addListener("menuClick:" + id, listener);
|
||||
this.sendMessage("GM_registerMenuCommand", [id, name, accessKey]);
|
||||
return id;
|
||||
}
|
||||
|
||||
@GMContext.API()
|
||||
GM_addStyle(css: string) {
|
||||
// 与content页的消息通讯实际是同步,此方法不需要经过background
|
||||
// 这里直接使用同步的方式去处理, 不要有promise
|
||||
const resp = (<CustomEventMessage>this.message).syncSendMessage({
|
||||
action: this.prefix + "/runtime/gmApi",
|
||||
data: {
|
||||
uuid: this.scriptRes.uuid,
|
||||
api: "GM_addElement",
|
||||
params: [
|
||||
null,
|
||||
"style",
|
||||
{
|
||||
textContent: css,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (resp.code !== 0) {
|
||||
throw new Error(resp.message);
|
||||
}
|
||||
return (<CustomEventMessage>this.message).getAndDelRelatedTarget(resp.data);
|
||||
}
|
||||
|
||||
@GMContext.API()
|
||||
GM_addElement(parentNode: EventTarget | string, tagName: any, attrs?: any) {
|
||||
// 与content页的消息通讯实际是同步,此方法不需要经过background
|
||||
// 这里直接使用同步的方式去处理, 不要有promise
|
||||
let parentNodeId: any = parentNode;
|
||||
if (typeof parentNodeId !== "string") {
|
||||
const id = (<CustomEventMessage>this.message).sendRelatedTarget(parentNodeId);
|
||||
parentNodeId = id;
|
||||
} else {
|
||||
parentNodeId = null;
|
||||
}
|
||||
const resp = (<CustomEventMessage>this.message).syncSendMessage({
|
||||
action: this.prefix + "/runtime/gmApi",
|
||||
data: {
|
||||
uuid: this.scriptRes.uuid,
|
||||
api: "GM_addElement",
|
||||
params: [
|
||||
parentNodeId,
|
||||
typeof parentNode === "string" ? parentNode : tagName,
|
||||
typeof parentNode === "string" ? tagName : attrs,
|
||||
],
|
||||
},
|
||||
});
|
||||
if (resp.code !== 0) {
|
||||
throw new Error(resp.message);
|
||||
}
|
||||
return (<CustomEventMessage>this.message).getAndDelRelatedTarget(resp.data);
|
||||
}
|
||||
|
||||
@GMContext.API()
|
||||
GM_unregisterMenuCommand(id: number): void {
|
||||
if (!this.menuMap) {
|
||||
this.menuMap = new Map();
|
||||
}
|
||||
this.menuMap.delete(id);
|
||||
this.EE.removeAllListeners("menuClick" + id);
|
||||
this.EE.removeAllListeners("menuClick:" + id);
|
||||
this.sendMessage("GM_unregisterMenuCommand", [id]);
|
||||
}
|
||||
|
||||
@GMContext.API()
|
||||
CAT_userConfig() {
|
||||
return this.sendMessage("CAT_userConfig", []);
|
||||
}
|
||||
|
||||
@GMContext.API({
|
||||
depend: ["CAT_fetchBlob", "CAT_createBlobUrl"],
|
||||
})
|
||||
async CAT_fileStorage(action: "list" | "download" | "upload" | "delete" | "config", details: any) {
|
||||
if (action === "config") {
|
||||
this.sendMessage("CAT_fileStorage", ["config"]);
|
||||
return;
|
||||
}
|
||||
const sendDetails: { [key: string]: string } = {
|
||||
baseDir: details.baseDir || "",
|
||||
path: details.path || "",
|
||||
filename: details.filename,
|
||||
file: details.file,
|
||||
};
|
||||
if (action === "upload") {
|
||||
const url = await this.CAT_createBlobUrl(details.data);
|
||||
sendDetails.data = url;
|
||||
}
|
||||
this.sendMessage("CAT_fileStorage", [action, sendDetails]).then(async (resp: { action: string; data: any }) => {
|
||||
switch (resp.action) {
|
||||
case "onload": {
|
||||
if (action === "download") {
|
||||
// 读取blob
|
||||
const blob = await this.CAT_fetchBlob(resp.data);
|
||||
details.onload && details.onload(blob);
|
||||
} else {
|
||||
details.onload && details.onload(resp.data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "error": {
|
||||
if (typeof resp.data.code === "undefined") {
|
||||
details.onerror && details.onerror({ code: -1, message: resp.data.message });
|
||||
return;
|
||||
}
|
||||
details.onerror && details.onerror(resp.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 用于脚本跨域请求,需要@connect domain指定允许的域名
|
||||
@GMContext.API({
|
||||
depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"],
|
||||
@@ -441,7 +540,7 @@ export default class GMApi {
|
||||
break;
|
||||
default:
|
||||
LoggerCore.logger().warn("GM_xmlhttpRequest resp is error", {
|
||||
action: data.action,
|
||||
data,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -458,4 +557,238 @@ 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"],
|
||||
})
|
||||
public async GM_notification(
|
||||
detail: GMTypes.NotificationDetails | string,
|
||||
ondone?: GMTypes.NotificationOnDone | string,
|
||||
image?: string,
|
||||
onclick?: GMTypes.NotificationOnClick
|
||||
) {
|
||||
this.eventId += 1;
|
||||
let data: GMTypes.NotificationDetails;
|
||||
if (typeof detail === "string") {
|
||||
data = {};
|
||||
data.text = detail;
|
||||
switch (arguments.length) {
|
||||
case 4:
|
||||
data.onclick = onclick;
|
||||
case 3:
|
||||
data.image = image;
|
||||
case 2:
|
||||
data.title = <string>ondone;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
data = Object.assign({}, detail);
|
||||
data.ondone = data.ondone || <GMTypes.NotificationOnDone>ondone;
|
||||
}
|
||||
let click: GMTypes.NotificationOnClick;
|
||||
let done: GMTypes.NotificationOnDone;
|
||||
let create: GMTypes.NotificationOnClick;
|
||||
if (data.onclick) {
|
||||
click = data.onclick;
|
||||
delete data.onclick;
|
||||
}
|
||||
if (data.ondone) {
|
||||
done = data.ondone;
|
||||
delete data.ondone;
|
||||
}
|
||||
if (data.oncreate) {
|
||||
create = data.oncreate;
|
||||
delete data.oncreate;
|
||||
}
|
||||
this.sendMessage("GM_notification", [data]).then((id) => {
|
||||
if (create) {
|
||||
create.apply({ id }, [id]);
|
||||
}
|
||||
this.EE.addListener("GM_notification:" + id, (resp: any) => {
|
||||
switch (resp.event) {
|
||||
case "click":
|
||||
case "buttonClick": {
|
||||
click && click.apply({ id }, [id, resp.params.index]);
|
||||
break;
|
||||
}
|
||||
case "close": {
|
||||
done && done.apply({ id }, [resp.params.byUser]);
|
||||
this.EE.removeAllListeners("GM_notification:" + this.eventId);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LoggerCore.logger().warn("GM_notification resp is error", {
|
||||
resp,
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@GMContext.API()
|
||||
public GM_closeNotification(id: string): void {
|
||||
this.sendMessage("GM_closeNotification", [id]);
|
||||
}
|
||||
|
||||
@GMContext.API()
|
||||
public GM_updateNotification(id: string, details: GMTypes.NotificationDetails): void {
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
const r = this.scriptRes.resource[name];
|
||||
if (r) {
|
||||
if (isBlobUrl) {
|
||||
return URL.createObjectURL(base64ToBlob(r.base64));
|
||||
}
|
||||
return r.base64;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
@@ -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.eventId, data.data);
|
||||
}
|
||||
});
|
||||
this.server.on("runtime/valueUpdate", (data: ValueUpdateData) => {
|
@@ -64,8 +64,9 @@ 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,
|
||||
emitEvent: GMApi.prototype.emitEvent,
|
||||
EE: new EventEmitter(),
|
||||
GM: { Info: GMInfo },
|
||||
GM_info: GMInfo,
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -7,12 +7,14 @@ import {
|
||||
SCRIPT_TYPE_BACKGROUND,
|
||||
ScriptRunResouce,
|
||||
} from "@App/app/repo/scripts";
|
||||
import ExecScript from "@App/runtime/content/exec_script";
|
||||
import { BgExecScriptWarp, CATRetryError } from "@App/runtime/content/exec_warp";
|
||||
import { Server } from "@Packages/message/server";
|
||||
import { WindowMessage } from "@Packages/message/window_message";
|
||||
import { CronJob } from "cron";
|
||||
import { proxyUpdateRunStatus } from "../offscreen/client";
|
||||
import { BgExecScriptWarp } from "../content/exec_warp";
|
||||
import ExecScript, { ValueUpdateData } from "../content/exec_script";
|
||||
import { getStorageName } from "@App/pkg/utils/utils";
|
||||
import { EmitEventRequest } from "../service_worker/runtime";
|
||||
|
||||
export class Runtime {
|
||||
cronJob: Map<string, Array<CronJob>> = new Map();
|
||||
@@ -204,7 +206,7 @@ export class Runtime {
|
||||
} else {
|
||||
this.cronJob.set(script.uuid, cronJobList);
|
||||
}
|
||||
return Promise.resolve(!flag);
|
||||
return !flag;
|
||||
}
|
||||
|
||||
crontabExec(script: ScriptRunResouce, oncePos: number) {
|
||||
@@ -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.eventId, 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));
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import { InstallSource } from ".";
|
||||
import { Resource } from "@App/app/repo/resource";
|
||||
import { MessageSend } from "@Packages/message/server";
|
||||
import { ScriptMenu, ScriptMenuItem } from "./popup";
|
||||
import PermissionVerify, { ConfirmParam, UserConfirm } from "./permission_verify";
|
||||
|
||||
export class ServiceWorkerClient extends Client {
|
||||
constructor(msg: MessageSend) {
|
||||
@@ -52,6 +53,10 @@ export class ScriptClient extends Client {
|
||||
excludeUrl(uuid: string, url: string, remove: boolean) {
|
||||
return this.do("excludeUrl", { uuid, url, remove });
|
||||
}
|
||||
|
||||
requestCheckUpdate(uuid: string) {
|
||||
return this.do("requestCheckUpdate", uuid);
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceClient extends Client {
|
||||
@@ -72,6 +77,10 @@ export class ValueClient extends Client {
|
||||
getScriptValue(script: Script) {
|
||||
return this.do("getScriptValue", script);
|
||||
}
|
||||
|
||||
setScriptValue(uuid: string, key: string, value: any) {
|
||||
return this.do("setScriptValue", { uuid, key, value });
|
||||
}
|
||||
}
|
||||
|
||||
export class RuntimeClient extends Client {
|
||||
@@ -119,9 +128,25 @@ export class PopupClient extends Client {
|
||||
return this.do("menuClick", {
|
||||
uuid,
|
||||
id: data.id,
|
||||
tabId: data.tabId,
|
||||
frameId: data.frameId,
|
||||
documentId: data.documentId,
|
||||
sender: {
|
||||
tabId: data.tabId,
|
||||
frameId: data.frameId,
|
||||
documentId: data.documentId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class PermissionClient extends Client {
|
||||
constructor(msg: MessageSend) {
|
||||
super(msg, "serviceWorker/runtime/permission");
|
||||
}
|
||||
|
||||
confirm(uuid: string, userConfirm: UserConfirm): Promise<void> {
|
||||
return this.do("confirm", { uuid, userConfirm });
|
||||
}
|
||||
|
||||
getPermissionInfo(uuid: string): ReturnType<PermissionVerify["getInfo"]> {
|
||||
return this.do("getInfo", uuid);
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,22 @@
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import { Script, ScriptDAO } from "@App/app/repo/scripts";
|
||||
import { GetSender, Group, MessageSend } from "@Packages/message/server";
|
||||
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 PermissionVerify, { ConfirmParam } from "./permission_verify";
|
||||
import { connect, sendMessage } 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";
|
||||
import { getIcon, isFirefox } from "@App/pkg/utils/utils";
|
||||
import { MockMessageConnect } from "@Packages/message/mock_message";
|
||||
import i18next, { i18nName } from "@App/locales/locales";
|
||||
import { SystemConfig } from "@App/pkg/config/config";
|
||||
import FileSystemFactory from "@Packages/filesystem/factory";
|
||||
import FileSystem from "@Packages/filesystem/filesystem";
|
||||
import { isWarpTokenError } from "@Packages/filesystem/error";
|
||||
import { joinPath } from "@Packages/filesystem/utils";
|
||||
|
||||
// GMApi,处理脚本的GM API调用请求
|
||||
|
||||
@@ -24,6 +31,41 @@ 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,
|
||||
};
|
||||
|
||||
type NotificationData = {
|
||||
uuid: string;
|
||||
details: GMTypes.NotificationDetails;
|
||||
sender: ExtMessageSender;
|
||||
};
|
||||
|
||||
export type Api = (request: Request, con: GetSender) => Promise<any>;
|
||||
|
||||
export default class GMApi {
|
||||
@@ -31,9 +73,9 @@ export default class GMApi {
|
||||
|
||||
scriptDAO: ScriptDAO = new ScriptDAO();
|
||||
|
||||
permissionVerify: PermissionVerify = new PermissionVerify();
|
||||
|
||||
constructor(
|
||||
private systemConfig: SystemConfig,
|
||||
private permissionVerify: PermissionVerify,
|
||||
private group: Group,
|
||||
private send: MessageSend,
|
||||
private mq: MessageQueue,
|
||||
@@ -43,37 +85,165 @@ export default class GMApi {
|
||||
this.logger = LoggerCore.logger().with({ service: "runtime/gm_api" });
|
||||
}
|
||||
|
||||
async handlerRequest(data: MessageRequest, con: GetSender) {
|
||||
async handlerRequest(data: MessageRequest, sender: GetSender) {
|
||||
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, con);
|
||||
return api.api.call(this, req, sender);
|
||||
}
|
||||
|
||||
// 解析请求
|
||||
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({
|
||||
confirm(request: Request) {
|
||||
if (request.params[0] === "store") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
const detail = <GMTypes.CookieDetails>request.params[1];
|
||||
if (!detail.url && !detail.domain) {
|
||||
return Promise.reject(new Error("there must be one of url or domain"));
|
||||
}
|
||||
let url: URL = <URL>{};
|
||||
if (detail.url) {
|
||||
url = new URL(detail.url);
|
||||
} else {
|
||||
url.host = detail.domain || "";
|
||||
url.hostname = detail.domain || "";
|
||||
}
|
||||
let flag = false;
|
||||
if (request.script.metadata.connect) {
|
||||
const { connect } = request.script.metadata;
|
||||
for (let i = 0; i < connect.length; i += 1) {
|
||||
if (url.hostname.endsWith(connect[i])) {
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!flag) {
|
||||
return Promise.reject(new Error("hostname must be in the definition of connect"));
|
||||
}
|
||||
const metadata: { [key: string]: string } = {};
|
||||
metadata[i18next.t("script_name")] = i18nName(request.script);
|
||||
metadata[i18next.t("request_domain")] = url.host;
|
||||
return Promise.resolve({
|
||||
permission: "cookie",
|
||||
permissionValue: url.host,
|
||||
title: i18next.t("access_cookie_content")!,
|
||||
metadata,
|
||||
describe: i18next.t("confirm_script_operation")!,
|
||||
permissionContent: i18next.t("cookie_domain")!,
|
||||
uuid: "",
|
||||
});
|
||||
},
|
||||
})
|
||||
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, {
|
||||
@@ -82,12 +252,125 @@ export default class GMApi {
|
||||
});
|
||||
}
|
||||
|
||||
@PermissionVerify.API()
|
||||
CAT_userConfig(request: Request) {
|
||||
chrome.tabs.create({
|
||||
url: `/src/options.html#/?userConfig=${request.uuid}`,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
||||
@PermissionVerify.API({
|
||||
confirm: (request: Request) => {
|
||||
const [action, details] = request.params;
|
||||
if (action === "config") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
const dir = details.baseDir ? details.baseDir : request.script.uuid;
|
||||
const metadata: { [key: string]: string } = {};
|
||||
metadata[i18next.t("script_name")] = i18nName(request.script);
|
||||
return Promise.resolve({
|
||||
permission: "file_storage",
|
||||
permissionValue: dir,
|
||||
title: i18next.t("script_operation_title"),
|
||||
metadata,
|
||||
describe: i18next.t("script_operation_description", { dir }),
|
||||
wildcard: false,
|
||||
permissionContent: i18next.t("script_permission_content"),
|
||||
} as ConfirmParam);
|
||||
},
|
||||
})
|
||||
async CAT_fileStorage(request: Request, sender: GetSender): Promise<{ action: string; data: any } | boolean> {
|
||||
const [action, details] = request.params;
|
||||
if (action === "config") {
|
||||
chrome.tabs.create({
|
||||
url: `/src/options.html#/setting`,
|
||||
active: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const fsConfig = await this.systemConfig.getCatFileStorage();
|
||||
if (fsConfig.status === "unset") {
|
||||
return { action: "error", data: { code: 1, error: "file storage is unset" } };
|
||||
}
|
||||
if (fsConfig.status === "error") {
|
||||
return { action: "error", data: { code: 2, error: "file storage is error" } };
|
||||
}
|
||||
let fs: FileSystem;
|
||||
const baseDir = `ScriptCat/app/${details.baseDir ? details.baseDir : request.script.uuid}`;
|
||||
try {
|
||||
fs = await FileSystemFactory.create(fsConfig.filesystem, fsConfig.params[fsConfig.filesystem]);
|
||||
await FileSystemFactory.mkdirAll(fs, baseDir);
|
||||
fs = await fs.openDir(baseDir);
|
||||
} catch (e: any) {
|
||||
if (isWarpTokenError(e)) {
|
||||
fsConfig.status = "error";
|
||||
this.systemConfig.setCatFileStorage(fsConfig);
|
||||
return { action: "error", data: { code: 2, error: e.error.message } };
|
||||
}
|
||||
return { action: "error", data: { code: 8, error: e.message } };
|
||||
}
|
||||
switch (action) {
|
||||
case "list":
|
||||
try {
|
||||
const list = await fs.list();
|
||||
list.forEach((file) => {
|
||||
(<any>file).absPath = file.path;
|
||||
file.path = joinPath(file.path.substring(file.path.indexOf(baseDir) + baseDir.length));
|
||||
});
|
||||
return { action: "onload", data: list };
|
||||
} catch (e: any) {
|
||||
return { action: "error", data: { code: 3, error: e.message } };
|
||||
}
|
||||
case "upload":
|
||||
try {
|
||||
const w = await fs.create(details.path);
|
||||
await w.write(await (await fetch(<string>details.data)).blob());
|
||||
return { action: "onload", data: true };
|
||||
} catch (e: any) {
|
||||
return { action: "error", data: { code: 4, error: e.message } };
|
||||
}
|
||||
case "download":
|
||||
try {
|
||||
const info = <CATType.FileStorageFileInfo>details.file;
|
||||
fs = await fs.openDir(`${info.path}`);
|
||||
const r = await fs.open({
|
||||
fsid: (<any>info).fsid,
|
||||
name: info.name,
|
||||
path: info.absPath,
|
||||
size: info.size,
|
||||
digest: info.digest,
|
||||
createtime: info.createtime,
|
||||
updatetime: info.updatetime,
|
||||
});
|
||||
const blob = await r.read("blob");
|
||||
const url = URL.createObjectURL(blob);
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 6000);
|
||||
return { action: "onload", data: url };
|
||||
} catch (e: any) {
|
||||
return { action: "error", data: { code: 5, error: e.message } };
|
||||
}
|
||||
break;
|
||||
case "delete":
|
||||
try {
|
||||
await fs.delete(`${details.path}`);
|
||||
return { action: "onload", data: true };
|
||||
} catch (e: any) {
|
||||
return { action: "error", data: { code: 6, error: e.message } };
|
||||
}
|
||||
default:
|
||||
throw new Error("action is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
// 根据header生成dnr规则
|
||||
async buildDNRRule(reqeustId: number, params: GMSend.XHRDetails): Promise<{ [key: string]: string }> {
|
||||
// 检查是否有unsafe header,有则生成dnr规则
|
||||
const headers = params.headers;
|
||||
if (!headers) {
|
||||
return Promise.resolve({});
|
||||
return {};
|
||||
}
|
||||
const requestHeaders = [
|
||||
{
|
||||
@@ -116,6 +399,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;
|
||||
@@ -141,16 +444,109 @@ export default class GMApi {
|
||||
removeRuleIds: [ruleId],
|
||||
addRules: [rule],
|
||||
});
|
||||
return Promise.resolve(headers);
|
||||
return headers;
|
||||
}
|
||||
|
||||
gmXhrHeadersReceived: EventEmitter = new EventEmitter();
|
||||
|
||||
// TODO: maxRedirects实现
|
||||
@PermissionVerify.API()
|
||||
async GM_xmlhttpRequest(request: Request, con: GetSender) {
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@PermissionVerify.API({
|
||||
confirm: async (request: Request) => {
|
||||
const config = <GMSend.XHRDetails>request.params[0];
|
||||
const url = new URL(config.url);
|
||||
if (request.script.metadata.connect) {
|
||||
const { connect } = request.script.metadata;
|
||||
for (let i = 0; i < connect.length; i += 1) {
|
||||
if (url.hostname.endsWith(connect[i])) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
const metadata: { [key: string]: string } = {};
|
||||
metadata[i18next.t("script_name")] = i18nName(request.script);
|
||||
metadata[i18next.t("request_domain")] = url.hostname;
|
||||
metadata[i18next.t("request_url")] = config.url;
|
||||
|
||||
return Promise.resolve({
|
||||
permission: "cors",
|
||||
permissionValue: url.hostname,
|
||||
title: i18next.t("script_accessing_cross_origin_resource"),
|
||||
metadata,
|
||||
describe: i18next.t("confirm_operation_description"),
|
||||
wildcard: true,
|
||||
permissionContent: i18next.t("domain"),
|
||||
} as ConfirmParam);
|
||||
},
|
||||
})
|
||||
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
|
||||
@@ -162,26 +558,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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -194,7 +599,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,11 +612,268 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
@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) {
|
||||
throw new Error("param is failed");
|
||||
}
|
||||
const details: GMTypes.NotificationDetails = request.params[0];
|
||||
const options: chrome.notifications.NotificationOptions<true> = {
|
||||
title: details.title || "ScriptCat",
|
||||
message: details.text || "无消息内容",
|
||||
iconUrl: details.image || getIcon(request.script) || chrome.runtime.getURL("assets/logo.png"),
|
||||
type: isFirefox() || details.progress === undefined ? "basic" : "progress",
|
||||
};
|
||||
if (!isFirefox()) {
|
||||
options.silent = details.silent;
|
||||
options.buttons = details.buttons;
|
||||
}
|
||||
options.progress = options.progress && parseInt(details.progress as any, 10);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
chrome.notifications.create(options, (notificationId) => {
|
||||
Cache.getInstance().set(`GM_notification:${notificationId}`, {
|
||||
uuid: request.script.uuid,
|
||||
details: details,
|
||||
sender: sender.getExtMessageSender(),
|
||||
});
|
||||
if (details.timeout) {
|
||||
setTimeout(() => {
|
||||
chrome.notifications.clear(notificationId);
|
||||
Cache.getInstance().del(`GM_notification:${notificationId}`);
|
||||
}, details.timeout);
|
||||
}
|
||||
resolve(notificationId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@PermissionVerify.API({
|
||||
link: "GM_notification",
|
||||
})
|
||||
GM_closeNotification(request: Request) {
|
||||
if (request.params.length === 0) {
|
||||
throw new Error("param is failed");
|
||||
}
|
||||
const [notificationId] = request.params;
|
||||
Cache.getInstance().del(`GM_notification:${notificationId}`);
|
||||
chrome.notifications.clear(notificationId);
|
||||
}
|
||||
|
||||
@PermissionVerify.API({
|
||||
link: "GM_notification",
|
||||
})
|
||||
GM_updateNotification(request: Request) {
|
||||
if (isFirefox()) {
|
||||
throw new Error("firefox does not support this method");
|
||||
}
|
||||
const id = request.params[0];
|
||||
const details: GMTypes.NotificationDetails = request.params[1];
|
||||
const options: chrome.notifications.NotificationOptions = {
|
||||
title: details.title,
|
||||
message: details.text,
|
||||
iconUrl: details.image,
|
||||
type: details.progress === undefined ? "basic" : "progress",
|
||||
silent: details.silent,
|
||||
progress: details.progress && parseInt(details.progress as any, 10),
|
||||
};
|
||||
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;
|
||||
if (ret) {
|
||||
this.runtime.emitEventToTab(ret.sender, {
|
||||
event: "GM_notification",
|
||||
eventId: notificationId,
|
||||
uuid: ret.uuid,
|
||||
data: {
|
||||
event,
|
||||
params,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
chrome.notifications.onClosed.addListener((notificationId, byUser) => {
|
||||
send("close", notificationId, {
|
||||
byUser,
|
||||
});
|
||||
Cache.getInstance().del(`GM_notification:${notificationId}`);
|
||||
});
|
||||
chrome.notifications.onClicked.addListener((notificationId) => {
|
||||
send("click", notificationId);
|
||||
});
|
||||
chrome.notifications.onButtonClicked.addListener((notificationId, index) => {
|
||||
send("buttonClick", notificationId, {
|
||||
index: index,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 处理GM_xmlhttpRequest请求
|
||||
handlerGmXhr() {
|
||||
chrome.webRequest.onBeforeSendHeaders.addListener(
|
||||
@@ -262,5 +924,26 @@ export default class GMApi {
|
||||
start() {
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { ValueService } from "./value";
|
||||
import { RuntimeService } from "./runtime";
|
||||
import { ServiceWorkerMessageSend } from "@Packages/message/window_message";
|
||||
import { PopupService } from "./popup";
|
||||
import { SystemConfig } from "@App/pkg/config/config";
|
||||
|
||||
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
|
||||
|
||||
@@ -24,15 +25,26 @@ export default class ServiceWorkerManager {
|
||||
this.mq.emit("preparationOffscreen", {});
|
||||
});
|
||||
|
||||
const systemConfig = new SystemConfig(this.mq);
|
||||
|
||||
const resource = new ResourceService(this.api.group("resource"), this.mq);
|
||||
resource.init();
|
||||
const value = new ValueService(this.api.group("value"), this.sender);
|
||||
const script = new ScriptService(this.api.group("script"), this.mq, value, resource);
|
||||
const script = new ScriptService(systemConfig, this.api.group("script"), this.mq, value, resource);
|
||||
script.init();
|
||||
const runtime = new RuntimeService(this.api.group("runtime"), this.sender, this.mq, value, script);
|
||||
const runtime = new RuntimeService(systemConfig, this.api.group("runtime"), this.sender, this.mq, value, script);
|
||||
runtime.init();
|
||||
const popup = new PopupService(this.api.group("popup"), this.mq, runtime);
|
||||
popup.init();
|
||||
value.init(runtime, popup);
|
||||
|
||||
// 定时器处理
|
||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||
switch (alarm.name) {
|
||||
case "checkScriptUpdate":
|
||||
script.checkScriptUpdate();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { Api, Request } from "./gm_api";
|
||||
import Queue from "@App/pkg/utils/queue";
|
||||
import CacheKey from "@App/app/cache_key";
|
||||
import { Permission, PermissionDAO } from "@App/app/repo/permission";
|
||||
import { Group } from "@Packages/message/server";
|
||||
|
||||
export interface ConfirmParam {
|
||||
// 权限名
|
||||
@@ -36,8 +37,6 @@ export interface ApiParam {
|
||||
background?: boolean;
|
||||
// 是否需要弹出页面让用户进行确认
|
||||
confirm?: (request: Request) => Promise<boolean | ConfirmParam>;
|
||||
// 监听方法
|
||||
listener?: () => void;
|
||||
// 别名
|
||||
alias?: string[];
|
||||
// 关联
|
||||
@@ -59,9 +58,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,
|
||||
@@ -100,21 +96,18 @@ export default class PermissionVerify {
|
||||
reject: (reason: any) => void;
|
||||
}> = new Queue();
|
||||
|
||||
async removePermissionCache(scriptId: number) {
|
||||
async removePermissionCache(uuid: string) {
|
||||
// 先删除缓存
|
||||
(await Cache.getInstance().list()).forEach((key) => {
|
||||
if (key.startsWith(`permission:${scriptId.toString()}:`)) {
|
||||
if (key.startsWith(`permission:${uuid}:`)) {
|
||||
Cache.getInstance().del(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private permissionDAO: PermissionDAO;
|
||||
private permissionDAO: PermissionDAO = new PermissionDAO();
|
||||
|
||||
constructor() {
|
||||
this.permissionDAO = new PermissionDAO();
|
||||
this.dealConfirmQueue();
|
||||
}
|
||||
constructor(private group: Group) {}
|
||||
|
||||
// 验证是否有权限
|
||||
verify(request: Request, api: ApiValue): Promise<boolean> {
|
||||
@@ -254,9 +247,72 @@ export default class PermissionVerify {
|
||||
|
||||
// 弹出窗口让用户进行确认
|
||||
async confirmWindow(script: Script, confirm: ConfirmParam): Promise<UserConfirm> {
|
||||
return Promise.resolve({
|
||||
allow: true,
|
||||
type: 1,
|
||||
return new Promise((resolve, reject) => {
|
||||
const uuid = uuidv4();
|
||||
// 超时处理
|
||||
const timeout = setTimeout(() => {
|
||||
this.confirmMap.delete(uuid);
|
||||
reject(new Error("permission confirm timeout"));
|
||||
}, 40 * 1000);
|
||||
// 保存到map中
|
||||
this.confirmMap.set(uuid, {
|
||||
confirm,
|
||||
script,
|
||||
resolve: (value: UserConfirm) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(value);
|
||||
},
|
||||
reject,
|
||||
});
|
||||
// 打开窗口
|
||||
chrome.tabs.create({
|
||||
url: chrome.runtime.getURL(`src/confirm.html?uuid=${uuid}`),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 处理确认
|
||||
private userConfirm(data: { uuid: string; userConfirm: UserConfirm }) {
|
||||
const confirm = this.confirmMap.get(data.uuid);
|
||||
if (!confirm) {
|
||||
if (data.userConfirm.type === 0) {
|
||||
// 忽略
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
return Promise.reject(new Error("confirm not found"));
|
||||
}
|
||||
this.confirmMap.delete(data.uuid);
|
||||
confirm.resolve(data.userConfirm);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// 获取信息
|
||||
private getInfo(uuid: string) {
|
||||
const data = this.confirmMap.get(uuid);
|
||||
if (!data) {
|
||||
return Promise.reject(new Error("permission confirm not found"));
|
||||
}
|
||||
const { script, confirm } = data;
|
||||
// 查询允许统配的有多少个相同等待确认权限
|
||||
let likeNum = 0;
|
||||
if (data.confirm.wildcard) {
|
||||
this.confirmQueue.list.forEach((value) => {
|
||||
const confirm = value.confirm as ConfirmParam;
|
||||
if (
|
||||
confirm.wildcard &&
|
||||
value.request.uuid === data.script.uuid &&
|
||||
confirm.permission === data.confirm.permission
|
||||
) {
|
||||
likeNum += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ script, confirm, likeNum });
|
||||
}
|
||||
|
||||
init() {
|
||||
this.dealConfirmQueue();
|
||||
this.group.on("confirm", this.userConfirm.bind(this));
|
||||
this.group.on("getInfo", this.getInfo.bind(this));
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { Group } from "@Packages/message/server";
|
||||
import { ExtMessageSender, Group } from "@Packages/message/server";
|
||||
import { RuntimeService, ScriptMatchInfo } from "./runtime";
|
||||
import Cache from "@App/app/cache";
|
||||
import { GetPopupDataReq, GetPopupDataRes } from "./client";
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -206,7 +208,7 @@ export class PopupService {
|
||||
|
||||
// 事务更新脚本菜单
|
||||
txUpdateScriptMenu(tabId: number, callback: (menu: ScriptMenu[]) => Promise<any>) {
|
||||
return Cache.getInstance().tx("tabScript:" + tabId, async (menu) => {
|
||||
return Cache.getInstance().tx<ScriptMenu[]>("tabScript:" + tabId, async (menu) => {
|
||||
return callback(menu || []);
|
||||
});
|
||||
}
|
||||
@@ -252,14 +254,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,55 +297,33 @@ 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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
menuClick({
|
||||
uuid,
|
||||
id,
|
||||
tabId,
|
||||
frameId,
|
||||
documentId,
|
||||
}: {
|
||||
uuid: string;
|
||||
id: number;
|
||||
tabId: number;
|
||||
frameId: number;
|
||||
documentId: string;
|
||||
}) {
|
||||
menuClick({ uuid, id, sender }: { uuid: string; id: number; sender: ExtMessageSender }) {
|
||||
// 菜单点击事件
|
||||
this.runtime.sendMessageToTab(
|
||||
tabId,
|
||||
"menuClick",
|
||||
{
|
||||
uuid,
|
||||
id,
|
||||
tabId,
|
||||
},
|
||||
{
|
||||
frameId,
|
||||
documentId: documentId,
|
||||
}
|
||||
);
|
||||
this.runtime.emitEventToTab(sender, {
|
||||
uuid,
|
||||
event: "menuClick",
|
||||
eventId: id.toString(),
|
||||
});
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
@@ -356,9 +337,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;
|
||||
});
|
||||
});
|
||||
// 监听页面切换加载菜单
|
||||
@@ -372,16 +363,25 @@ 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!,
|
||||
frameId: menuItem.frameId || 0,
|
||||
documentId: menuItem.documentId || "",
|
||||
sender: {
|
||||
tabId: bgscript ? -1 : tab!.id!,
|
||||
frameId: menuItem.frameId || 0,
|
||||
documentId: menuItem.documentId || "",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@@ -193,6 +193,19 @@ export class ResourceService {
|
||||
(u.hash.sha512 && u.hash.sha512 !== resource.hash.sha512)
|
||||
) {
|
||||
resource.content = `console.warn("ScriptCat: couldn't load resource from URL ${url} due to a SRI error ");`;
|
||||
// 尝试重新加载
|
||||
this.loadByUrl(u.url, resource.type).then((reloadRes) => {
|
||||
this.logger.info("reload resource success", {
|
||||
url: u.url,
|
||||
hash: {
|
||||
expected: u.hash,
|
||||
old: resource.hash,
|
||||
new: reloadRes.hash,
|
||||
},
|
||||
});
|
||||
reloadRes.updatetime = new Date().getTime();
|
||||
this.resourceDAO.save(reloadRes);
|
||||
});
|
||||
}
|
||||
}
|
||||
return Promise.resolve(resource);
|
||||
@@ -214,12 +227,13 @@ export class ResourceService {
|
||||
sha512: "",
|
||||
});
|
||||
} else {
|
||||
const wordArray = crypto.lib.WordArray.create(<ArrayBuffer>reader.result);
|
||||
resolve({
|
||||
md5: crypto.MD5(<string>reader.result).toString(),
|
||||
sha1: crypto.SHA1(<string>reader.result).toString(),
|
||||
sha256: crypto.SHA256(<string>reader.result).toString(),
|
||||
sha384: crypto.SHA384(<string>reader.result).toString(),
|
||||
sha512: crypto.SHA512(<string>reader.result).toString(),
|
||||
md5: crypto.MD5(wordArray).toString(),
|
||||
sha1: crypto.SHA1(wordArray).toString(),
|
||||
sha256: crypto.SHA256(wordArray).toString(),
|
||||
sha384: crypto.SHA384(wordArray).toString(),
|
||||
sha512: crypto.SHA512(wordArray).toString(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { GetSender, Group, MessageSend } from "@Packages/message/server";
|
||||
import { ExtMessageSender, GetSender, Group, MessageSend } from "@Packages/message/server";
|
||||
import {
|
||||
Script,
|
||||
SCRIPT_STATUS,
|
||||
@@ -16,11 +16,16 @@ 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";
|
||||
import { PopupService } from "./popup";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import PermissionVerify from "./permission_verify";
|
||||
import { SystemConfig } from "@App/pkg/config/config";
|
||||
|
||||
// 为了优化性能,存储到缓存时删除了code与value
|
||||
export interface ScriptMatchInfo extends ScriptRunResouce {
|
||||
@@ -29,6 +34,13 @@ export interface ScriptMatchInfo extends ScriptRunResouce {
|
||||
customizeExcludeMatches: string[];
|
||||
}
|
||||
|
||||
export interface EmitEventRequest {
|
||||
uuid: string;
|
||||
event: string;
|
||||
eventId: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export class RuntimeService {
|
||||
scriptDAO: ScriptDAO = new ScriptDAO();
|
||||
|
||||
@@ -37,6 +49,7 @@ export class RuntimeService {
|
||||
scriptMatchCache: Map<string, ScriptMatchInfo> | null | undefined;
|
||||
|
||||
constructor(
|
||||
private systemConfig: SystemConfig,
|
||||
private group: Group,
|
||||
private sender: MessageSend,
|
||||
private mq: MessageQueue,
|
||||
@@ -46,7 +59,9 @@ export class RuntimeService {
|
||||
|
||||
async init() {
|
||||
// 启动gm api
|
||||
const gmApi = new GMApi(this.group, this.sender, this.mq, this.value, this);
|
||||
const permission = new PermissionVerify(this.group.group("permission"));
|
||||
const gmApi = new GMApi(this.systemConfig, permission, this.group, this.sender, this.mq, this.value, this);
|
||||
permission.init();
|
||||
gmApi.start();
|
||||
|
||||
this.group.on("stopScript", this.stopScript.bind(this));
|
||||
@@ -116,20 +131,35 @@ export class RuntimeService {
|
||||
}
|
||||
|
||||
// 给指定tab发送消息
|
||||
sendMessageToTab(
|
||||
tabId: number,
|
||||
action: string,
|
||||
data: any,
|
||||
options?: {
|
||||
documentId?: string;
|
||||
frameId?: number;
|
||||
}
|
||||
) {
|
||||
if (tabId === -1) {
|
||||
sendMessageToTab(to: ExtMessageSender, action: string, data: any) {
|
||||
if (to.tabId === -1) {
|
||||
// 如果是-1, 代表给offscreen发送消息
|
||||
return sendMessage(this.sender, "offscreen/runtime/" + action, data);
|
||||
}
|
||||
return sendMessage(new ExtensionContentMessageSend(tabId, options), "content/runtime/" + action, data);
|
||||
return sendMessage(
|
||||
new ExtensionContentMessageSend(to.tabId, {
|
||||
documentId: to.documentId,
|
||||
frameId: to.frameId,
|
||||
}),
|
||||
"content/runtime/" + action,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
// 给指定脚本触发事件
|
||||
emitEventToTab(to: ExtMessageSender, req: EmitEventRequest) {
|
||||
if (to.tabId === -1) {
|
||||
// 如果是-1, 代表给offscreen发送消息
|
||||
return sendMessage(this.sender, "offscreen/runtime/emitEvent", req);
|
||||
}
|
||||
return sendMessage(
|
||||
new ExtensionContentMessageSend(to.tabId, {
|
||||
documentId: to.documentId,
|
||||
frameId: to.frameId,
|
||||
}),
|
||||
"content/runtime/emitEvent",
|
||||
req
|
||||
);
|
||||
}
|
||||
|
||||
async getPageScriptUuidByUrl(url: string, includeCustomize?: boolean) {
|
||||
@@ -222,6 +252,10 @@ export class RuntimeService {
|
||||
registerInjectScript() {
|
||||
chrome.userScripts.getScripts({ ids: ["scriptcat-inject"] }).then((res) => {
|
||||
if (res.length == 0) {
|
||||
chrome.userScripts.configureWorld({
|
||||
csp: "script-src 'self' 'unsafe-inline' 'unsafe-eval' *",
|
||||
messaging: true,
|
||||
});
|
||||
fetch("inject.js")
|
||||
.then((res) => res.text())
|
||||
.then(async (injectJs) => {
|
||||
@@ -236,18 +270,17 @@ export class RuntimeService {
|
||||
world: "MAIN",
|
||||
runAt: "document_start",
|
||||
},
|
||||
// 注册content
|
||||
{
|
||||
id: "scriptcat-content",
|
||||
js: [{ file: "src/content.js" }],
|
||||
matches: ["<all_urls>"],
|
||||
allFrames: true,
|
||||
runAt: "document_start",
|
||||
world: "USER_SCRIPT",
|
||||
},
|
||||
]);
|
||||
});
|
||||
chrome.scripting.registerContentScripts([
|
||||
{
|
||||
id: "scriptcat-content",
|
||||
js: ["src/content.js"],
|
||||
matches: ["<all_urls>"],
|
||||
allFrames: true,
|
||||
runAt: "document_start",
|
||||
world: "ISOLATED",
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -366,22 +399,27 @@ export class RuntimeService {
|
||||
world: "MAIN",
|
||||
};
|
||||
|
||||
// 排除由loadPage时决定, 不使用userScript的excludeMatches处理
|
||||
if (script.metadata["exclude"]) {
|
||||
const excludeMatches = script.metadata["exclude"];
|
||||
const result = dealPatternMatches(excludeMatches);
|
||||
const result = dealPatternMatches(excludeMatches, {
|
||||
exclude: true,
|
||||
});
|
||||
|
||||
registerScript.excludeMatches = result.patternResult;
|
||||
// registerScript.excludeMatches = result.patternResult;
|
||||
scriptMatchInfo.excludeMatches = result.result;
|
||||
}
|
||||
// 自定义排除
|
||||
if (script.selfMetadata && script.selfMetadata.exclude) {
|
||||
const excludeMatches = script.selfMetadata.exclude;
|
||||
const result = dealPatternMatches(excludeMatches);
|
||||
const result = dealPatternMatches(excludeMatches, {
|
||||
exclude: true,
|
||||
});
|
||||
|
||||
if (!registerScript.excludeMatches) {
|
||||
registerScript.excludeMatches = [];
|
||||
}
|
||||
registerScript.excludeMatches.push(...result.patternResult);
|
||||
// registerScript.excludeMatches.push(...result.patternResult);
|
||||
scriptMatchInfo.customizeExcludeMatches = result.result;
|
||||
}
|
||||
|
||||
@@ -399,7 +437,18 @@ export class RuntimeService {
|
||||
if (await Cache.getInstance().get("registryScript:" + script.uuid)) {
|
||||
await chrome.userScripts.update([registerScript]);
|
||||
} else {
|
||||
await chrome.userScripts.register([registerScript]);
|
||||
await chrome.userScripts.register([registerScript], () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
LoggerCore.logger().error("registerScript error", {
|
||||
error: chrome.runtime.lastError,
|
||||
name: script.name,
|
||||
registerMatch: {
|
||||
matches: registerScript.matches,
|
||||
excludeMatches: registerScript.excludeMatches,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
await Cache.getInstance().set("registryScript:" + script.uuid, true);
|
||||
}
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { fetchScriptInfo } from "@App/pkg/utils/script";
|
||||
import { fetchScriptInfo, prepareScriptByCode } from "@App/pkg/utils/script";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Group } from "@Packages/message/server";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Cache from "@App/app/cache";
|
||||
import CacheKey from "@App/app/cache_key";
|
||||
import { openInCurrentTab, randomString } from "@App/pkg/utils/utils";
|
||||
import { checkSilenceUpdate, ltever, openInCurrentTab, randomString } from "@App/pkg/utils/utils";
|
||||
import {
|
||||
Script,
|
||||
SCRIPT_RUN_STATUS,
|
||||
@@ -19,7 +19,8 @@ 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";
|
||||
import { SystemConfig } from "@App/pkg/config/config";
|
||||
|
||||
export class ScriptService {
|
||||
logger: Logger;
|
||||
@@ -27,6 +28,7 @@ export class ScriptService {
|
||||
scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO();
|
||||
|
||||
constructor(
|
||||
private systemConfig: SystemConfig,
|
||||
private group: Group,
|
||||
private mq: MessageQueue,
|
||||
private valueService: ValueService,
|
||||
@@ -305,6 +307,117 @@ export class ScriptService {
|
||||
});
|
||||
}
|
||||
|
||||
async checkUpdate(uuid: string, source: "user" | "system") {
|
||||
// 检查更新
|
||||
const script = await this.scriptDAO.get(uuid);
|
||||
if (!script) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
await this.scriptDAO.update(uuid, { checktime: new Date().getTime() });
|
||||
if (!script.checkUpdateUrl) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const logger = LoggerCore.logger({
|
||||
uuid: script.uuid,
|
||||
name: script.name,
|
||||
});
|
||||
try {
|
||||
const info = await fetchScriptInfo(script.checkUpdateUrl, source, false, script.uuid);
|
||||
const { metadata } = info;
|
||||
if (!metadata) {
|
||||
logger.error("parse metadata failed");
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const newVersion = metadata.version && metadata.version[0];
|
||||
if (!newVersion) {
|
||||
logger.error("parse version failed", { version: "" });
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
let oldVersion = script.metadata.version && script.metadata.version[0];
|
||||
if (!oldVersion) {
|
||||
oldVersion = "0.0.0";
|
||||
}
|
||||
// 对比版本大小
|
||||
if (ltever(newVersion, oldVersion, logger)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
// 进行更新
|
||||
this.openUpdatePage(script, source);
|
||||
} catch (e) {
|
||||
logger.error("check update failed", Logger.E(e));
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// 打开更新窗口
|
||||
public openUpdatePage(script: Script, source: "user" | "system") {
|
||||
const logger = this.logger.with({
|
||||
uuid: script.uuid,
|
||||
name: script.name,
|
||||
downloadUrl: script.downloadUrl,
|
||||
checkUpdateUrl: script.checkUpdateUrl,
|
||||
});
|
||||
fetchScriptInfo(script.downloadUrl || script.checkUpdateUrl!, source, true, script.uuid)
|
||||
.then(async (info) => {
|
||||
// 是否静默更新
|
||||
if (await this.systemConfig.getSilenceUpdateScript()) {
|
||||
try {
|
||||
const prepareScript = await prepareScriptByCode(
|
||||
info.code,
|
||||
script.downloadUrl || script.checkUpdateUrl!,
|
||||
script.uuid
|
||||
);
|
||||
if (checkSilenceUpdate(prepareScript.oldScript!.metadata, prepareScript.script.metadata)) {
|
||||
logger.info("silence update script");
|
||||
this.installScript({
|
||||
script: prepareScript.script,
|
||||
code: info.code,
|
||||
upsertBy: source,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("prepare script failed", Logger.E(e));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 打开安装页面
|
||||
Cache.getInstance().set(CacheKey.scriptInstallInfo(info.uuid), info);
|
||||
chrome.tabs.create({
|
||||
url: `/src/install.html?uuid=${info.uuid}`,
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("fetch script info failed", Logger.E(e));
|
||||
});
|
||||
}
|
||||
|
||||
checkScriptUpdate() {
|
||||
this.scriptDAO.all().then(async (scripts) => {
|
||||
const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle();
|
||||
if (!checkCycle) {
|
||||
return;
|
||||
}
|
||||
const check = await this.systemConfig.getUpdateDisableScript();
|
||||
scripts.forEach(async (script) => {
|
||||
// 是否检查禁用脚本
|
||||
if (!check && script.status === SCRIPT_STATUS_DISABLE) {
|
||||
return;
|
||||
}
|
||||
// 检查是否符合
|
||||
if (script.checktime + checkCycle * 1000 > Date.now()) {
|
||||
return;
|
||||
}
|
||||
this.checkUpdate(script.uuid, "system");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
requestCheckUpdate(uuid: string) {
|
||||
return this.checkUpdate(uuid, "user");
|
||||
}
|
||||
|
||||
init() {
|
||||
this.listenerScriptInstall();
|
||||
|
||||
@@ -317,5 +430,12 @@ export class ScriptService {
|
||||
this.group.on("getCode", this.getCode.bind(this));
|
||||
this.group.on("getScriptRunResource", this.buildScriptRunResource.bind(this));
|
||||
this.group.on("excludeUrl", this.excludeUrl.bind(this));
|
||||
this.group.on("requestCheckUpdate", this.requestCheckUpdate.bind(this));
|
||||
|
||||
// 定时检查更新, 每10分钟检查一次
|
||||
chrome.alarms.create("checkScriptUpdate", {
|
||||
delayInMinutes: 10,
|
||||
periodInMinutes: 10,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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 { GetSender, 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,28 +67,45 @@ 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(
|
||||
{
|
||||
tabId: tab.id!,
|
||||
},
|
||||
"valueUpdate",
|
||||
sendData
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 推送到offscreen中
|
||||
sendMessage(this.send, "offscreen/runtime/valueUpdate", sendData);
|
||||
}
|
||||
});
|
||||
// 推送到offscreen中
|
||||
this.runtime!.sendMessageToTab(
|
||||
{
|
||||
tabId: -1,
|
||||
},
|
||||
"valueUpdate",
|
||||
sendData
|
||||
);
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
setScriptValue(data: { uuid: string; key: string; value: any }, sender: GetSender) {
|
||||
return this.setValue(data.uuid, data.key, data.value, {
|
||||
runFlag: "user",
|
||||
tabId: -2,
|
||||
});
|
||||
}
|
||||
|
||||
init(runtime: RuntimeService, popup: PopupService) {
|
||||
this.popup = popup;
|
||||
this.runtime = runtime;
|
||||
this.group.on("getScriptValue", this.getScriptValue.bind(this));
|
||||
this.group.on("setScriptValue", this.setScriptValue.bind(this));
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
|
||||
|
92
src/linter.worker.ts
Normal file
92
src/linter.worker.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
//@ts-ignore
|
||||
import { Linter } from "eslint-linter-browserify";
|
||||
import { userscriptsRules } from "../packages/eslint/linter-config";
|
||||
|
||||
// eslint语法检查,使用webworker
|
||||
|
||||
const linter = new Linter();
|
||||
|
||||
// 额外定义 userscripts 规则
|
||||
linter.defineRules(userscriptsRules);
|
||||
|
||||
const rules = linter.getRules();
|
||||
|
||||
const severityMap = {
|
||||
2: 8, // 2 for ESLint is error
|
||||
1: 4, // 1 for ESLint is warning
|
||||
};
|
||||
|
||||
function getTextBlock(text: string, startPosition: number, endPosition: number) {
|
||||
if (startPosition > endPosition || startPosition < 0 || endPosition > text.length) {
|
||||
throw new Error("Invalid positions provided");
|
||||
}
|
||||
|
||||
let startLineNumber = 1;
|
||||
let startColumn = 1;
|
||||
let endLineNumber = 1;
|
||||
let endColumn = 1;
|
||||
|
||||
for (let i = 0, currentLine = 1, currentColumn = 1; i < text.length; i += 1) {
|
||||
if (i === startPosition) {
|
||||
startLineNumber = currentLine;
|
||||
startColumn = currentColumn;
|
||||
}
|
||||
|
||||
if (i === endPosition) {
|
||||
endLineNumber = currentLine;
|
||||
endColumn = currentColumn;
|
||||
break;
|
||||
}
|
||||
|
||||
if (text[i] === "\n") {
|
||||
currentLine += 1;
|
||||
currentColumn = 0;
|
||||
}
|
||||
|
||||
currentColumn += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
startLineNumber,
|
||||
startColumn,
|
||||
endLineNumber,
|
||||
endColumn,
|
||||
};
|
||||
}
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
const { code, id, config } = event.data;
|
||||
const errs = linter.verify(code, config);
|
||||
const markers = errs.map((err: any) => {
|
||||
const rule = rules.get(err.ruleId);
|
||||
let target = "";
|
||||
if (rule) {
|
||||
target = rule.meta.docs.url;
|
||||
}
|
||||
let fix: any;
|
||||
if (err.fix) {
|
||||
fix = {
|
||||
range: getTextBlock(code, err.fix.range[0], err.fix.range[1]),
|
||||
text: err.fix.text,
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: {
|
||||
value: err.ruleId || "",
|
||||
target,
|
||||
},
|
||||
startLineNumber: err.line,
|
||||
endLineNumber: err.endLine || err.line,
|
||||
startColumn: err.column,
|
||||
endColumn: err.endColumn || err.column,
|
||||
message: err.message,
|
||||
// 设置错误的等级,此处ESLint与monaco的存在差异,做一层映射
|
||||
// @ts-ignore
|
||||
severity: severityMap[err.severity],
|
||||
source: "ESLint",
|
||||
fix,
|
||||
};
|
||||
});
|
||||
// 发回主进程
|
||||
self.postMessage({ markers, id });
|
||||
});
|
@@ -358,5 +358,6 @@
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
"menu_expand_num_before": "Menu item more than",
|
||||
"menu_expand_num_after": "Auto-hide."
|
||||
"menu_expand_num_after": "Auto-hide.",
|
||||
"eslint_config_format_error": "ESLint configuration format error"
|
||||
}
|
@@ -10,10 +10,11 @@ import zhTW from "./zh-TW/translation.json";
|
||||
import achUG from "./ach-UG/translation.json";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import "dayjs/locale/zh-tw";
|
||||
import { systemConfig } from "@App/pages/store/global";
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
fallbackLng: "zh-CN",
|
||||
lng: localStorage.language || chrome.i18n.getUILanguage(),
|
||||
lng: chrome.i18n.getUILanguage(),
|
||||
interpolation: {
|
||||
escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
|
||||
},
|
||||
@@ -26,22 +27,13 @@ i18n.use(initReactI18next).init({
|
||||
},
|
||||
});
|
||||
|
||||
if (!localStorage.language) {
|
||||
chrome.i18n.getAcceptLanguages((lngs) => {
|
||||
// 遍历数组寻找匹配语言
|
||||
for (let i = 0; i < lngs.length; i += 1) {
|
||||
const lng = lngs[i];
|
||||
if (i18n.hasResourceBundle(lng, "translation")) {
|
||||
localStorage.language = lng;
|
||||
i18n.changeLanguage(lng);
|
||||
dayjs.locale(lng.toLocaleLowerCase());
|
||||
break;
|
||||
}
|
||||
}
|
||||
chrome.i18n.getAcceptLanguages((lngs) => {
|
||||
systemConfig.getLanguage().then((lng) => {
|
||||
i18n.changeLanguage(lng);
|
||||
dayjs.locale(lng.toLocaleLowerCase());
|
||||
});
|
||||
} else {
|
||||
dayjs.locale((localStorage.language as string).toLocaleLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export function i18nName(script: { name: string; metadata: Metadata }) {
|
||||
|
@@ -363,5 +363,6 @@
|
||||
"expand": "展开",
|
||||
"menu_expand_num_before": "菜单项超过",
|
||||
"menu_expand_num_after": "个时,自动隐藏",
|
||||
"script_name_cannot_be_set_to_empty": "脚本name不可以设置为空"
|
||||
"script_name_cannot_be_set_to_empty": "脚本name不可以设置为空",
|
||||
"eslint_config_format_error": "eslint配置格式错误"
|
||||
}
|
@@ -23,13 +23,18 @@
|
||||
"default_locale": "zh_CN",
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"alarms",
|
||||
"storage",
|
||||
"cookies",
|
||||
"offscreen",
|
||||
"scripting",
|
||||
"downloads",
|
||||
"activeTab",
|
||||
"webRequest",
|
||||
"userScripts",
|
||||
"contextMenus",
|
||||
"notifications",
|
||||
"clipboardWrite",
|
||||
"unlimitedStorage",
|
||||
"declarativeNetRequest"
|
||||
],
|
||||
|
@@ -4,9 +4,10 @@ import DBWriter from "./app/logger/db_writer";
|
||||
import { LoggerDAO } from "./app/repo/logger";
|
||||
import { OffscreenManager } from "./app/service/offscreen";
|
||||
|
||||
// 初始化数据库
|
||||
migrate();
|
||||
|
||||
function main() {
|
||||
// 初始化数据库
|
||||
migrate();
|
||||
// 初始化日志组件
|
||||
const loggerCore = new LoggerCore({
|
||||
writer: new DBWriter(new LoggerDAO()),
|
||||
|
@@ -3,6 +3,7 @@ import { LinterWorker } from "@App/pkg/utils/monaco-editor";
|
||||
import { useAppSelector } from "@App/pages/store/hooks";
|
||||
import { editor, Range } from "monaco-editor";
|
||||
import React, { useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||
import { globalCache, systemConfig } from "@App/pages/store/global";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@@ -18,10 +19,26 @@ const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.IStandaloneCod
|
||||
) => {
|
||||
const settings = useAppSelector((state) => state.setting);
|
||||
const [monacoEditor, setEditor] = useState<editor.IStandaloneCodeEditor>();
|
||||
const [enableEslint, setEnableEslint] = useState(false);
|
||||
const [eslintConfig, setEslintConfig] = useState("");
|
||||
|
||||
const div = useRef<HTMLDivElement>(null);
|
||||
useImperativeHandle(ref, () => ({
|
||||
editor: monacoEditor,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfigs = async () => {
|
||||
const [eslintConfig, enableEslint] = await Promise.all([
|
||||
systemConfig.getEslintConfig(),
|
||||
systemConfig.getEnableEslint(),
|
||||
]);
|
||||
setEslintConfig(eslintConfig);
|
||||
setEnableEslint(enableEslint);
|
||||
};
|
||||
loadConfigs();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (diffCode === undefined || code === undefined || !div.current) {
|
||||
return () => {};
|
||||
@@ -70,8 +87,7 @@ const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.IStandaloneCod
|
||||
}, [div, code, diffCode, editable, id]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {};
|
||||
if (!settings.eslint.enable) {
|
||||
if (!enableEslint) {
|
||||
return () => {};
|
||||
}
|
||||
if (!monacoEditor) {
|
||||
@@ -89,7 +105,7 @@ const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.IStandaloneCod
|
||||
LinterWorker.sendLinterMessage({
|
||||
code: model.getValue(),
|
||||
id,
|
||||
config: JSON.parse(settings.eslint.config),
|
||||
config: JSON.parse(eslintConfig),
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
@@ -183,7 +199,7 @@ const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.IStandaloneCod
|
||||
}
|
||||
}
|
||||
);
|
||||
Cache.getInstance().set("eslint-fix", fix);
|
||||
globalCache.set("eslint-fix", fix);
|
||||
|
||||
// 在行号旁显示ESLint错误/警告图标
|
||||
const formatMarkers = message.markers.map(
|
||||
@@ -203,7 +219,7 @@ const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.IStandaloneCod
|
||||
return () => {
|
||||
LinterWorker.hook.removeListener("message", handler);
|
||||
};
|
||||
}, [id, monacoEditor, settings.eslint.config, settings.eslint.enable]);
|
||||
}, [id, monacoEditor, enableEslint, eslintConfig]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import React from "react";
|
||||
import { Input, Select, Space } from "@arco-design/web-react";
|
||||
import FileSystemFactory, { FileSystemType } from "@Packages/filesystem/factory";
|
||||
|
||||
const fsParams = FileSystemFactory.params();
|
||||
|
||||
const fileSystemList: {
|
||||
key: FileSystemType;
|
||||
@@ -65,10 +67,7 @@ const FileSystemParams: React.FC<{
|
||||
<>
|
||||
<span>{fsParams[fileSystemType][key].title}</span>
|
||||
<Select
|
||||
value={
|
||||
fileSystemParams[key] ||
|
||||
fsParams[fileSystemType][key].options![0]
|
||||
}
|
||||
value={fileSystemParams[key] || fsParams[fileSystemType][key].options![0]}
|
||||
onChange={(value) => {
|
||||
onChangeFileSystemParams({
|
||||
...fileSystemParams,
|
||||
|
@@ -1,29 +1,29 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Collapse,
|
||||
Link,
|
||||
Message,
|
||||
Space,
|
||||
Typography,
|
||||
} from "@arco-design/web-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Card, Collapse, Link, Message, Space, Typography } from "@arco-design/web-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FileSystemParams from "../FileSystemParams";
|
||||
import { systemConfig } from "@App/pages/store/global";
|
||||
import FileSystemFactory, { FileSystemType } from "@Packages/filesystem/factory";
|
||||
import { set } from "node_modules/yaml/dist/schema/yaml-1.1/set";
|
||||
|
||||
const CollapseItem = Collapse.Item;
|
||||
|
||||
const GMApiSetting: React.FC = () => {
|
||||
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
|
||||
const [status, setStatus] = useState(systemConfig.catFileStorage.status);
|
||||
const [fileSystemType, setFilesystemType] = useState<FileSystemType>(
|
||||
systemConfig.catFileStorage.filesystem
|
||||
);
|
||||
const [status, setStatus] = useState("unset");
|
||||
const [fileSystemType, setFilesystemType] = useState<FileSystemType>("webdav");
|
||||
const [fileSystemParams, setFilesystemParam] = useState<{
|
||||
[key: string]: any;
|
||||
}>(systemConfig.catFileStorage.params[fileSystemType] || {});
|
||||
}>({});
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
systemConfig.getCatFileStorage().then((res) => {
|
||||
setStatus(res.status);
|
||||
setFilesystemType(res.filesystem);
|
||||
setFilesystemParam(res.params[res.filesystem] || {});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card title={t("gm_api")} bordered={false}>
|
||||
<Collapse bordered={false} defaultActiveKey={["storage"]}>
|
||||
@@ -48,21 +48,18 @@ const GMApiSetting: React.FC = () => {
|
||||
type="primary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await FileSystemFactory.create(
|
||||
fileSystemType,
|
||||
fileSystemParams
|
||||
);
|
||||
await FileSystemFactory.create(fileSystemType, fileSystemParams);
|
||||
} catch (e) {
|
||||
Message.error(`${t("account_validation_failed")}: ${e}`);
|
||||
return;
|
||||
}
|
||||
const params = { ...systemConfig.catFileStorage.params };
|
||||
const params = { ...fileSystemParams };
|
||||
params[fileSystemType] = fileSystemParams;
|
||||
systemConfig.catFileStorage = {
|
||||
systemConfig.setCatFileStorage({
|
||||
status: "success",
|
||||
filesystem: fileSystemType,
|
||||
params,
|
||||
};
|
||||
});
|
||||
setStatus("success");
|
||||
Message.success(t("save_success")!);
|
||||
}}
|
||||
@@ -72,10 +69,14 @@ const GMApiSetting: React.FC = () => {
|
||||
<Button
|
||||
key="reset"
|
||||
onClick={() => {
|
||||
const config = systemConfig.catFileStorage;
|
||||
config.status = "unset";
|
||||
systemConfig.catFileStorage = config;
|
||||
systemConfig.setCatFileStorage({
|
||||
status: "unset",
|
||||
filesystem: "webdav",
|
||||
params: {},
|
||||
});
|
||||
setStatus("unset");
|
||||
setFilesystemParam({});
|
||||
setFilesystemType("webdav");
|
||||
}}
|
||||
type="primary"
|
||||
status="danger"
|
||||
@@ -87,10 +88,7 @@ const GMApiSetting: React.FC = () => {
|
||||
type="secondary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
let fs = await FileSystemFactory.create(
|
||||
fileSystemType,
|
||||
fileSystemParams
|
||||
);
|
||||
let fs = await FileSystemFactory.create(fileSystemType, fileSystemParams);
|
||||
fs = await fs.openDir("ScriptCat/app");
|
||||
window.open(await fs.getDirUrl(), "_black");
|
||||
} catch (e) {
|
||||
@@ -110,17 +108,9 @@ const GMApiSetting: React.FC = () => {
|
||||
setFilesystemParam(params);
|
||||
}}
|
||||
/>
|
||||
{status === "unset" && (
|
||||
<Typography.Text type="secondary">{t("not_set")}</Typography.Text>
|
||||
)}
|
||||
{status === "success" && (
|
||||
<Typography.Text type="success">{t("in_use")}</Typography.Text>
|
||||
)}
|
||||
{status === "error" && (
|
||||
<Typography.Text type="error">
|
||||
{t("storage_error")}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{status === "unset" && <Typography.Text type="secondary">{t("not_set")}</Typography.Text>}
|
||||
{status === "success" && <Typography.Text type="success">{t("in_use")}</Typography.Text>}
|
||||
{status === "error" && <Typography.Text type="error">{t("storage_error")}</Typography.Text>}
|
||||
</Space>
|
||||
</CollapseItem>
|
||||
</Collapse>
|
||||
|
@@ -14,12 +14,11 @@ import { RiPlayFill, RiStopFill } from "react-icons/ri";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ScriptIcons } from "@App/pages/options/routes/utils";
|
||||
import { ScriptMenu, ScriptMenuItem } from "@App/app/service/service_worker/popup";
|
||||
import { selectMenuExpandNum } from "@App/pages/store/features/setting";
|
||||
import { useAppSelector } from "@App/pages/store/hooks";
|
||||
import { popupClient, runtimeClient, scriptClient } from "@App/pages/store/features/script";
|
||||
import { i18nName } from "@App/locales/locales";
|
||||
import { subscribeScriptRunStatus } from "@App/app/service/queue";
|
||||
import { messageQueue } from "@App/pages/store/global";
|
||||
import { messageQueue, systemConfig } from "@App/pages/store/global";
|
||||
|
||||
const CollapseItem = Collapse.Item;
|
||||
|
||||
@@ -46,7 +45,7 @@ const ScriptMenuList: React.FC<{
|
||||
[key: string]: boolean;
|
||||
}>({});
|
||||
const { t } = useTranslation();
|
||||
const menuExpandNum = useAppSelector(selectMenuExpandNum);
|
||||
const [menuExpandNum, setMenuExpandNum] = useState(5);
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
@@ -70,6 +69,10 @@ const ScriptMenuList: React.FC<{
|
||||
return newList;
|
||||
});
|
||||
});
|
||||
// 获取配置
|
||||
systemConfig.getMenuExpandNum().then((num) => {
|
||||
setMenuExpandNum(num);
|
||||
});
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
|
@@ -1,18 +1,10 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next"; // 添加这行导入语句
|
||||
import { Script, UserConfig } from "@App/app/repo/scripts";
|
||||
import {
|
||||
Checkbox,
|
||||
Form,
|
||||
FormInstance,
|
||||
Input,
|
||||
InputNumber,
|
||||
Message,
|
||||
Modal,
|
||||
Select,
|
||||
Tabs,
|
||||
} from "@arco-design/web-react";
|
||||
import { Checkbox, Form, FormInstance, Input, InputNumber, Message, Modal, Select, Tabs } from "@arco-design/web-react";
|
||||
import TabPane from "@arco-design/web-react/es/Tabs/tab-pane";
|
||||
import { ValueClient } from "@App/app/service/service_worker/client";
|
||||
import { message } from "@App/pages/store/global";
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -41,17 +33,13 @@ const UserConfigPanel: React.FC<{
|
||||
if (formRefs.current[tab]) {
|
||||
const saveValues = formRefs.current[tab].getFieldsValue();
|
||||
// 更新value
|
||||
const valueCtrl = IoC.instance(ValueController) as ValueController;
|
||||
const valueClient = new ValueClient(message);
|
||||
Object.keys(saveValues).forEach((key) => {
|
||||
Object.keys(saveValues[key]).forEach((valueKey) => {
|
||||
if (saveValues[key][valueKey] === undefined) {
|
||||
return;
|
||||
}
|
||||
valueCtrl.setValue(
|
||||
script.id,
|
||||
`${key}.${valueKey}`,
|
||||
saveValues[key][valueKey]
|
||||
);
|
||||
valueClient.setScriptValue(script.uuid, `${key}.${valueKey}`, saveValues[key][valueKey]);
|
||||
});
|
||||
});
|
||||
Message.success(t("save_success")!); // 替换为键值对应的英文文本
|
||||
@@ -73,7 +61,7 @@ const UserConfigPanel: React.FC<{
|
||||
return (
|
||||
<TabPane key={itemKey} title={itemKey}>
|
||||
<Form
|
||||
key={script.id}
|
||||
key={script.uuid}
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
@@ -85,11 +73,7 @@ const UserConfigPanel: React.FC<{
|
||||
}}
|
||||
>
|
||||
{Object.keys(value).map((key) => (
|
||||
<FormItem
|
||||
key={key}
|
||||
label={value[key].title}
|
||||
field={`${itemKey}.${key}`}
|
||||
>
|
||||
<FormItem key={key} label={value[key].title} field={`${itemKey}.${key}`}>
|
||||
{() => {
|
||||
const item = value[key];
|
||||
let { type } = item;
|
||||
@@ -112,20 +96,9 @@ const UserConfigPanel: React.FC<{
|
||||
switch (type) {
|
||||
case "text":
|
||||
if (item.password) {
|
||||
return (
|
||||
<Input.Password
|
||||
placeholder={item.description}
|
||||
maxLength={item.max}
|
||||
/>
|
||||
);
|
||||
return <Input.Password placeholder={item.description} maxLength={item.max} />;
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
placeholder={item.description}
|
||||
maxLength={item.max}
|
||||
showWordLimit
|
||||
/>
|
||||
);
|
||||
return <Input placeholder={item.description} maxLength={item.max} showWordLimit />;
|
||||
case "number":
|
||||
return (
|
||||
<InputNumber
|
||||
@@ -136,13 +109,7 @@ const UserConfigPanel: React.FC<{
|
||||
/>
|
||||
);
|
||||
case "checkbox":
|
||||
return (
|
||||
<Checkbox
|
||||
defaultChecked={values[`${itemKey}.${key}`]}
|
||||
>
|
||||
{item.description}
|
||||
</Checkbox>
|
||||
);
|
||||
return <Checkbox defaultChecked={values[`${itemKey}.${key}`]}>{item.description}</Checkbox>;
|
||||
case "select":
|
||||
case "mult-select":
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
@@ -159,11 +126,7 @@ const UserConfigPanel: React.FC<{
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
mode={
|
||||
item.type === "mult-select"
|
||||
? "multiple"
|
||||
: undefined
|
||||
}
|
||||
mode={item.type === "mult-select" ? "multiple" : undefined}
|
||||
placeholder={item.description}
|
||||
>
|
||||
{options!.map((option) => (
|
||||
|
@@ -16,7 +16,7 @@ import React, { ReactNode, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./index.css";
|
||||
import { useAppDispatch, useAppSelector } from "@App/pages/store/hooks";
|
||||
import { selectThemeMode, setDarkMode } from "@App/pages/store/features/setting";
|
||||
import { selectThemeMode, setDarkMode } from "@App/pages/store/features/config";
|
||||
import { RiFileCodeLine, RiImportLine, RiPlayListAddLine, RiTerminalBoxLine, RiTimerLine } from "react-icons/ri";
|
||||
|
||||
const MainLayout: React.FC<{
|
||||
|
144
src/pages/confirm/App.tsx
Normal file
144
src/pages/confirm/App.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { ConfirmParam } from "@App/app/service/service_worker/permission_verify";
|
||||
import { Button, Message, Space } from "@arco-design/web-react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { permissionClient } from "../store/features/script";
|
||||
|
||||
function App() {
|
||||
const uuid = window.location.search.split("=")[1];
|
||||
const [confirm, setConfirm] = React.useState<ConfirmParam>();
|
||||
const [likeNum, setLikeNum] = React.useState(0);
|
||||
const [second, setSecond] = React.useState(30);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (second === 0) {
|
||||
window.close();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setSecond(second - 1);
|
||||
}, 1000);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("beforeunload", () => {
|
||||
permissionClient.confirm(uuid, {
|
||||
allow: false,
|
||||
type: 0,
|
||||
});
|
||||
});
|
||||
|
||||
permissionClient
|
||||
.getPermissionInfo(uuid)
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
setConfirm(data.confirm);
|
||||
setLikeNum(data.likeNum);
|
||||
})
|
||||
.catch((e: any) => {
|
||||
Message.error(e.message || t("get_confirm_error"));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConfirm = (allow: boolean, type: number) => {
|
||||
return async () => {
|
||||
try {
|
||||
await permissionClient.confirm(uuid, {
|
||||
allow,
|
||||
type,
|
||||
});
|
||||
window.close();
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || t("confirm_error"));
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<Space direction="vertical">
|
||||
<span className="text-2xl font-500">{confirm?.title}</span>
|
||||
{confirm &&
|
||||
confirm.metadata &&
|
||||
Object.keys(confirm.metadata).map((key) => (
|
||||
<span className="text-base" key={key}>
|
||||
{key}: {confirm!.metadata![key]}
|
||||
</span>
|
||||
))}
|
||||
<span className="text-xl font-500">{confirm?.describe}</span>
|
||||
<div>
|
||||
<Button type="primary" onClick={handleConfirm(false, 1)}>
|
||||
{t("ignore")} ({second})
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Space>
|
||||
<Button onClick={handleConfirm(true, 1)} status="success">
|
||||
{t("allow_once")}
|
||||
</Button>
|
||||
<Button onClick={handleConfirm(true, 3)} status="success">
|
||||
{t("temporary_allow", {
|
||||
permissionContent: confirm?.permissionContent,
|
||||
})}
|
||||
</Button>
|
||||
{likeNum > 2 && (
|
||||
<Button onClick={handleConfirm(true, 2)} status="success">
|
||||
{t("temporary_allow_all", {
|
||||
permissionContent: confirm?.permissionContent,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleConfirm(true, 5)} status="success">
|
||||
{t("permanent_allow", {
|
||||
permissionContent: confirm?.permissionContent,
|
||||
})}
|
||||
</Button>
|
||||
{likeNum > 2 && (
|
||||
<Button onClick={handleConfirm(true, 4)} status="success">
|
||||
{t("permanent_allow_all", {
|
||||
permissionContent: confirm?.permissionContent,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
<div>
|
||||
<Space>
|
||||
<Button onClick={handleConfirm(false, 1)} status="danger">
|
||||
{t("deny_once")}
|
||||
</Button>
|
||||
<Button onClick={handleConfirm(false, 3)} status="danger">
|
||||
{t("temporary_deny", {
|
||||
permissionContent: confirm?.permissionContent,
|
||||
})}
|
||||
</Button>
|
||||
{likeNum > 2 && (
|
||||
<Button onClick={handleConfirm(false, 2)} status="danger">
|
||||
{t("temporary_deny_all", {
|
||||
permissionContent: confirm?.permissionContent,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleConfirm(false, 5)} status="danger">
|
||||
{t("permanent_deny", {
|
||||
permissionContent: confirm?.permissionContent,
|
||||
})}
|
||||
</Button>
|
||||
{likeNum > 2 && (
|
||||
<Button onClick={handleConfirm(false, 4)} status="danger">
|
||||
{t("permanent_deny_all", {
|
||||
permissionContent: confirm?.permissionContent,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
33
src/pages/confirm/main.tsx
Normal file
33
src/pages/confirm/main.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import MainLayout from "../components/layout/MainLayout.tsx";
|
||||
import "@arco-design/web-react/dist/css/arco.css";
|
||||
import "@App/locales/locales";
|
||||
import "@App/index.css";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "@App/pages/store/store.ts";
|
||||
import LoggerCore from "@App/app/logger/core.ts";
|
||||
import migrate from "@App/app/migrate.ts";
|
||||
import { LoggerDAO } from "@App/app/repo/logger.ts";
|
||||
import DBWriter from "@App/app/logger/db_writer.ts";
|
||||
|
||||
// 初始化数据库
|
||||
migrate();
|
||||
// 初始化日志组件
|
||||
const loggerCore = new LoggerCore({
|
||||
writer: new DBWriter(new LoggerDAO()),
|
||||
labels: { env: "install" },
|
||||
});
|
||||
|
||||
loggerCore.logger().debug("page start");
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<MainLayout className="!flex-col !px-4 box-border">
|
||||
<App />
|
||||
</MainLayout>
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
);
|
@@ -25,6 +25,7 @@ import {
|
||||
SCRIPT_STATUS_ENABLE,
|
||||
SCRIPT_TYPE_BACKGROUND,
|
||||
SCRIPT_TYPE_NORMAL,
|
||||
ScriptDAO,
|
||||
UserConfig,
|
||||
} from "@App/app/repo/scripts";
|
||||
import {
|
||||
@@ -68,7 +69,7 @@ import CloudScriptPlan from "@App/pages/components/CloudScriptPlan";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { nextTime, semTime } from "@App/pkg/utils/utils";
|
||||
import { i18nName } from "@App/locales/locales";
|
||||
import { getValues, ListHomeRender, ScriptIcons } from "./utils";
|
||||
import { ListHomeRender, ScriptIcons } from "./utils";
|
||||
import { useAppDispatch, useAppSelector } from "@App/pages/store/hooks";
|
||||
import {
|
||||
requestEnableScript,
|
||||
@@ -79,8 +80,10 @@ import {
|
||||
sortScript,
|
||||
requestStopScript,
|
||||
requestRunScript,
|
||||
scriptClient,
|
||||
} from "@App/pages/store/features/script";
|
||||
import { selectScriptListColumnWidth } from "@App/pages/store/features/setting";
|
||||
import { message, systemConfig } from "@App/pages/store/global";
|
||||
import { ValueClient } from "@App/app/service/service_worker/client";
|
||||
|
||||
type ListType = Script & { loading?: boolean };
|
||||
|
||||
@@ -93,7 +96,6 @@ function ScriptList() {
|
||||
const [cloudScript, setCloudScript] = useState<Script>();
|
||||
const dispatch = useAppDispatch();
|
||||
const scriptList = useAppSelector(selectScripts);
|
||||
const scriptListColumnWidth = useAppSelector(selectScriptListColumnWidth);
|
||||
const inputRef = useRef<RefInputType>(null);
|
||||
const navigate = useNavigate();
|
||||
const openUserConfig = useSearchParams()[0].get("userConfig") || "";
|
||||
@@ -400,35 +402,36 @@ function ScriptList() {
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
// if (!script.checkUpdateUrl) {
|
||||
// Message.warning(t("update_not_supported")!);
|
||||
// return;
|
||||
// }
|
||||
// Message.info({
|
||||
// id: "checkupdate",
|
||||
// content: t("checking_for_updates"),
|
||||
// });
|
||||
// scriptCtrl
|
||||
// .checkUpdate(script.id)
|
||||
// .then((res) => {
|
||||
// if (res) {
|
||||
// Message.warning({
|
||||
// id: "checkupdate",
|
||||
// content: t("new_version_available"),
|
||||
// });
|
||||
// } else {
|
||||
// Message.success({
|
||||
// id: "checkupdate",
|
||||
// content: t("latest_version"),
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// .catch((e) => {
|
||||
// Message.error({
|
||||
// id: "checkupdate",
|
||||
// content: `${t("update_check_failed")}: ${e.message}`,
|
||||
// });
|
||||
// });
|
||||
if (!script.checkUpdateUrl) {
|
||||
Message.warning(t("update_not_supported")!);
|
||||
return;
|
||||
}
|
||||
Message.info({
|
||||
id: "checkupdate",
|
||||
content: t("checking_for_updates"),
|
||||
});
|
||||
scriptClient
|
||||
.requestCheckUpdate(script.uuid)
|
||||
.then((res) => {
|
||||
console.log("res", res);
|
||||
if (res) {
|
||||
Message.warning({
|
||||
id: "checkupdate",
|
||||
content: t("new_version_available"),
|
||||
});
|
||||
} else {
|
||||
Message.success({
|
||||
id: "checkupdate",
|
||||
content: t("latest_version"),
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
Message.error({
|
||||
id: "checkupdate",
|
||||
content: `${t("update_check_failed")}: ${e.message}`,
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
{semTime(new Date(col))}
|
||||
@@ -474,7 +477,7 @@ function ScriptList() {
|
||||
type="text"
|
||||
icon={<RiSettings3Fill />}
|
||||
onClick={() => {
|
||||
getValues(item).then((newValues) => {
|
||||
new ValueClient(message).getScriptValue(item).then((newValues) => {
|
||||
setUserConfig({
|
||||
userConfig: { ...item.config! },
|
||||
script: item,
|
||||
@@ -546,23 +549,27 @@ function ScriptList() {
|
||||
// 设置列和判断是否打开用户配置
|
||||
useEffect(() => {
|
||||
if (openUserConfig) {
|
||||
const script = scriptList.find((item) => item.uuid === openUserConfig);
|
||||
if (script && script.config) {
|
||||
getValues(script).then((values) => {
|
||||
setUserConfig({
|
||||
script,
|
||||
userConfig: script.config!,
|
||||
values: values,
|
||||
const dao = new ScriptDAO();
|
||||
dao.get(openUserConfig).then((script) => {
|
||||
if (script && script.config) {
|
||||
new ValueClient(message).getScriptValue(script).then((values) => {
|
||||
setUserConfig({
|
||||
script,
|
||||
userConfig: script.config!,
|
||||
values: values,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setNewColumns(
|
||||
columns.map((item) => {
|
||||
item.width = scriptListColumnWidth[item.key!] ?? item.width;
|
||||
return item;
|
||||
})
|
||||
);
|
||||
systemConfig.getScriptListColumnWidth().then((columnWidth) => {
|
||||
setNewColumns(
|
||||
columns.map((item) => {
|
||||
item.width = columnWidth[item.key!] ?? item.width;
|
||||
return item;
|
||||
})
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 处理拖拽排序
|
||||
@@ -696,57 +703,33 @@ function ScriptList() {
|
||||
type="primary"
|
||||
size="mini"
|
||||
onClick={() => {
|
||||
const ids: number[] = [];
|
||||
const uuids: string[] = [];
|
||||
switch (action) {
|
||||
case "enable":
|
||||
select.forEach((item) => {
|
||||
scriptCtrl.enable(item.id).then(() => {
|
||||
const list = scriptList.map((script) => {
|
||||
if (script.id === item.id) {
|
||||
script.status = SCRIPT_STATUS_ENABLE;
|
||||
}
|
||||
return script;
|
||||
});
|
||||
setScriptList(list);
|
||||
});
|
||||
dispatch(requestEnableScript({ uuid: item.uuid, enable: true }));
|
||||
});
|
||||
break;
|
||||
case "disable":
|
||||
select.forEach((item) => {
|
||||
scriptCtrl.disable(item.id).then(() => {
|
||||
const list = scriptList.map((script) => {
|
||||
if (script.id === item.id) {
|
||||
script.status = SCRIPT_STATUS_DISABLE;
|
||||
}
|
||||
return script;
|
||||
});
|
||||
setScriptList(list);
|
||||
});
|
||||
dispatch(requestEnableScript({ uuid: item.uuid, enable: false }));
|
||||
});
|
||||
break;
|
||||
case "export":
|
||||
select.forEach((item) => {
|
||||
ids.push(item.id);
|
||||
uuids.push(item.uuid);
|
||||
});
|
||||
synchronizeCtrl.backup(ids);
|
||||
synchronizeCtrl.backup(uuids);
|
||||
break;
|
||||
case "delete":
|
||||
// eslint-disable-next-line no-restricted-globals, no-alert
|
||||
if (confirm(t("list.confirm_delete")!)) {
|
||||
select.forEach((item) => {
|
||||
scriptCtrl.delete(item.id).then(() => {
|
||||
setScriptList((list) => {
|
||||
return list.filter((script) => {
|
||||
return script.id !== item.id;
|
||||
});
|
||||
});
|
||||
});
|
||||
dispatch(requestDeleteScript(item.uuid));
|
||||
});
|
||||
}
|
||||
break;
|
||||
// 批量检查更新
|
||||
case "check_update":
|
||||
// eslint-disable-next-line no-restricted-globals, no-alert
|
||||
if (confirm(t("list.confirm_update")!)) {
|
||||
select.forEach((item, index, array) => {
|
||||
if (!item.checkUpdateUrl) {
|
||||
@@ -756,8 +739,8 @@ function ScriptList() {
|
||||
id: "checkupdateStart",
|
||||
content: t("starting_updates"),
|
||||
});
|
||||
scriptCtrl
|
||||
.checkUpdate(item.id)
|
||||
scriptClient
|
||||
.requestCheckUpdate(item.uuid)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
// 需要更新
|
||||
@@ -877,7 +860,7 @@ function ScriptList() {
|
||||
newColumns.forEach((column) => {
|
||||
newWidth[column.key! as string] = column.width as number;
|
||||
});
|
||||
systemConfig.scriptListColumnWidth = newWidth;
|
||||
systemConfig.setScriptListColumnWidth(newWidth);
|
||||
}}
|
||||
>
|
||||
{t("save")}
|
||||
|
@@ -1,38 +1,33 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Input,
|
||||
Message,
|
||||
Select,
|
||||
Space,
|
||||
} from "@arco-design/web-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Card, Checkbox, Input, Message, Select, Space } from "@arco-design/web-react";
|
||||
import Title from "@arco-design/web-react/es/Typography/title";
|
||||
import { IconQuestionCircleFill } from "@arco-design/web-react/icon";
|
||||
import { format } from "prettier";
|
||||
import babel from "prettier/parser-babel";
|
||||
import prettier from "prettier/standalone";
|
||||
import * as babel from "prettier/parser-babel";
|
||||
import prettierPluginEstree from "prettier/plugins/estree";
|
||||
import GMApiSetting from "@App/pages/components/GMApiSetting";
|
||||
import i18n from "@App/locales/locales";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import dayjs from "dayjs";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import FileSystemFactory, { FileSystemType } from "@Packages/filesystem/factory";
|
||||
import FileSystemParams from "@App/pages/components/FileSystemParams";
|
||||
import { systemConfig } from "@App/pages/store/global";
|
||||
|
||||
function Setting() {
|
||||
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
|
||||
const [syncDelete, setSyncDelete] = useState<boolean>(
|
||||
systemConfig.cloudSync.syncDelete
|
||||
);
|
||||
const [enableCloudSync, setEnableCloudSync] = useState(
|
||||
systemConfig.cloudSync.enable
|
||||
);
|
||||
const [fileSystemType, setFilesystemType] = useState<FileSystemType>(
|
||||
systemConfig.cloudSync.filesystem
|
||||
);
|
||||
const [syncDelete, setSyncDelete] = useState<boolean>();
|
||||
const [enableCloudSync, setEnableCloudSync] = useState<boolean>();
|
||||
const [fileSystemType, setFilesystemType] = useState<FileSystemType>("webdav");
|
||||
const [fileSystemParams, setFilesystemParam] = useState<{
|
||||
[key: string]: any;
|
||||
}>(systemConfig.cloudSync.params[fileSystemType] || {});
|
||||
}>({});
|
||||
const [language, setLanguage] = useState(i18n.language);
|
||||
const [menuExpandNum, setMenuExpandNum] = useState(5);
|
||||
const [checkScriptUpdateCycle, setCheckScriptUpdateCycle] = useState(0);
|
||||
const [updateDisableScript, setUpdateDisableScript] = useState(false);
|
||||
const [silenceUpdateScript, setSilenceUpdateScript] = useState(false);
|
||||
const [enableEslint, setEnableEslint] = useState(false);
|
||||
const [eslintConfig, setEslintConfig] = useState("");
|
||||
const languageList: { key: string; title: string }[] = [];
|
||||
const { t } = useTranslation();
|
||||
Object.keys(i18n.store.data).forEach((key) => {
|
||||
@@ -49,6 +44,44 @@ function Setting() {
|
||||
title: t("help_translate"),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfigs = async () => {
|
||||
const [
|
||||
cloudSync,
|
||||
menuExpandNum,
|
||||
checkCycle,
|
||||
updateDisabled,
|
||||
silenceUpdate,
|
||||
eslintConfig,
|
||||
enableEslint,
|
||||
language,
|
||||
] = await Promise.all([
|
||||
systemConfig.getCloudSync(),
|
||||
systemConfig.getMenuExpandNum(),
|
||||
systemConfig.getCheckScriptUpdateCycle(),
|
||||
systemConfig.getUpdateDisableScript(),
|
||||
systemConfig.getSilenceUpdateScript(),
|
||||
systemConfig.getEslintConfig(),
|
||||
systemConfig.getEnableEslint(),
|
||||
systemConfig.getLanguage(),
|
||||
]);
|
||||
|
||||
setSyncDelete(cloudSync.syncDelete);
|
||||
setEnableCloudSync(cloudSync.enable);
|
||||
setFilesystemType(cloudSync.filesystem);
|
||||
setFilesystemParam(cloudSync.params[cloudSync.filesystem] || {});
|
||||
setMenuExpandNum(menuExpandNum);
|
||||
setCheckScriptUpdateCycle(checkCycle);
|
||||
setUpdateDisableScript(updateDisabled);
|
||||
setSilenceUpdateScript(silenceUpdate);
|
||||
setEslintConfig(eslintConfig);
|
||||
setEnableEslint(enableEslint);
|
||||
setLanguage(language);
|
||||
};
|
||||
|
||||
loadConfigs();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Space
|
||||
className="setting"
|
||||
@@ -69,16 +102,11 @@ function Setting() {
|
||||
className="w-24"
|
||||
onChange={(value) => {
|
||||
if (value === "help") {
|
||||
window.open(
|
||||
"https://crowdin.com/project/scriptcat",
|
||||
"_blank"
|
||||
);
|
||||
window.open("https://crowdin.com/project/scriptcat", "_blank");
|
||||
return;
|
||||
}
|
||||
setLanguage(value);
|
||||
i18n.changeLanguage(value);
|
||||
dayjs.locale(value.toLocaleLowerCase());
|
||||
localStorage.language = value;
|
||||
systemConfig.setLanguage(value);
|
||||
Message.success(t("language_change_tip")!);
|
||||
}}
|
||||
>
|
||||
@@ -94,9 +122,11 @@ function Setting() {
|
||||
<Input
|
||||
style={{ width: "64px" }}
|
||||
type="number"
|
||||
defaultValue={systemConfig.menuExpandNum.toString()}
|
||||
value={menuExpandNum.toString()}
|
||||
onChange={(val) => {
|
||||
systemConfig.menuExpandNum = parseInt(val, 10);
|
||||
const num = parseInt(val, 10);
|
||||
setMenuExpandNum(num);
|
||||
systemConfig.setMenuExpandNum(num);
|
||||
}}
|
||||
/>
|
||||
{t("menu_expand_num_after")}
|
||||
@@ -134,16 +164,9 @@ function Setting() {
|
||||
if (enableCloudSync) {
|
||||
Message.info(t("cloud_sync_account_verification")!);
|
||||
try {
|
||||
await FileSystemFactory.create(
|
||||
fileSystemType,
|
||||
fileSystemParams
|
||||
);
|
||||
await FileSystemFactory.create(fileSystemType, fileSystemParams);
|
||||
} catch (e) {
|
||||
Message.error(
|
||||
`${t(
|
||||
"cloud_sync_verification_failed"
|
||||
)}: ${JSON.stringify(Logger.E(e))}`
|
||||
);
|
||||
Message.error(`${t("cloud_sync_verification_failed")}: ${JSON.stringify(Logger.E(e))}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -177,12 +200,14 @@ function Setting() {
|
||||
<Space>
|
||||
<span>{t("script_subscription_check_interval")}:</span>
|
||||
<Select
|
||||
defaultValue={systemConfig.checkScriptUpdateCycle.toString()}
|
||||
value={checkScriptUpdateCycle.toString()}
|
||||
style={{
|
||||
width: 120,
|
||||
}}
|
||||
onChange={(value) => {
|
||||
systemConfig.checkScriptUpdateCycle = parseInt(value, 10);
|
||||
const num = parseInt(value, 10);
|
||||
setCheckScriptUpdateCycle(num);
|
||||
systemConfig.setCheckScriptUpdateCycle(num);
|
||||
}}
|
||||
>
|
||||
<Select.Option value="0">{t("never")}</Select.Option>
|
||||
@@ -194,17 +219,19 @@ function Setting() {
|
||||
</Space>
|
||||
<Checkbox
|
||||
onChange={(checked) => {
|
||||
systemConfig.updateDisableScript = checked;
|
||||
setEnableCloudSync(checked);
|
||||
systemConfig.setUpdateDisableScript(checked);
|
||||
}}
|
||||
defaultChecked={systemConfig.updateDisableScript}
|
||||
checked={updateDisableScript}
|
||||
>
|
||||
{t("update_disabled_scripts")}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
onChange={(checked) => {
|
||||
systemConfig.silenceUpdateScript = checked;
|
||||
setSilenceUpdateScript(checked);
|
||||
systemConfig.setSilenceUpdateScript(checked);
|
||||
}}
|
||||
defaultChecked={systemConfig.silenceUpdateScript}
|
||||
checked={silenceUpdateScript}
|
||||
>
|
||||
{t("silent_update_non_critical_changes")}
|
||||
</Checkbox>
|
||||
@@ -215,9 +242,10 @@ function Setting() {
|
||||
<Space direction="vertical" className="w-full">
|
||||
<Checkbox
|
||||
onChange={(checked) => {
|
||||
systemConfig.enableEslint = checked;
|
||||
setEnableEslint(checked);
|
||||
systemConfig.setEnableEslint(checked);
|
||||
}}
|
||||
defaultChecked={systemConfig.enableEslint}
|
||||
checked={enableEslint}
|
||||
>
|
||||
{t("enable_eslint")}
|
||||
</Checkbox>
|
||||
@@ -246,12 +274,22 @@ function Setting() {
|
||||
minRows: 4,
|
||||
maxRows: 8,
|
||||
}}
|
||||
defaultValue={format(systemConfig.eslintConfig, {
|
||||
parser: "json",
|
||||
plugins: [babel],
|
||||
})}
|
||||
value={eslintConfig}
|
||||
onChange={(v) => {
|
||||
setEslintConfig(v);
|
||||
}}
|
||||
onBlur={(v) => {
|
||||
systemConfig.eslintConfig = v.target.value;
|
||||
prettier
|
||||
.format(eslintConfig, {
|
||||
parser: "json",
|
||||
plugins: [prettierPluginEstree, babel],
|
||||
})
|
||||
.then((res) => {
|
||||
systemConfig.setEslintConfig(v.target.value);
|
||||
})
|
||||
.catch((e) => {
|
||||
Message.error(`${t("eslint_config_format_error")}: ${JSON.stringify(Logger.E(e))}`);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
|
@@ -1,38 +1,33 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Drawer,
|
||||
Empty,
|
||||
Input,
|
||||
List,
|
||||
Message,
|
||||
Modal,
|
||||
Space,
|
||||
} from "@arco-design/web-react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, Card, Checkbox, Drawer, Empty, Input, List, Message, Modal, Space } from "@arco-design/web-react";
|
||||
import Title from "@arco-design/web-react/es/Typography/title";
|
||||
import { formatUnixTime } from "@App/pkg/utils/utils";
|
||||
import FileSystemParams from "@App/pages/components/FileSystemParams";
|
||||
import { IconQuestionCircleFill } from "@arco-design/web-react/icon";
|
||||
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FileSystemType } from "@Packages/filesystem/factory";
|
||||
import { systemConfig } from "@App/pages/store/global";
|
||||
|
||||
function Tools() {
|
||||
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
||||
const syncCtrl = IoC.instance(SynchronizeController) as SynchronizeController;
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
|
||||
const [fileSystemType, setFilesystemType] = useState<FileSystemType>(
|
||||
systemConfig.backup.filesystem
|
||||
);
|
||||
const [fileSystemType, setFilesystemType] = useState<FileSystemType>("webdav");
|
||||
const [fileSystemParams, setFilesystemParam] = useState<{
|
||||
[key: string]: any;
|
||||
}>(systemConfig.backup.params[fileSystemType] || {});
|
||||
}>({});
|
||||
const [backupFileList, setBackupFileList] = useState<File[]>([]);
|
||||
const vscodeRef = useRef<RefInputType>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
// 获取配置
|
||||
systemConfig.getBackup().then((backup) => {
|
||||
setFilesystemType(backup.filesystem);
|
||||
setFilesystemParam(backup.params[backup.filesystem] || {});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Space
|
||||
className="tools"
|
||||
@@ -48,12 +43,7 @@ function Tools() {
|
||||
<Space direction="vertical">
|
||||
<Title heading={6}>{t("local")}</Title>
|
||||
<Space>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileRef}
|
||||
style={{ display: "none" }}
|
||||
accept=".zip"
|
||||
/>
|
||||
<input type="file" ref={fileRef} style={{ display: "none" }} accept=".zip" />
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading.local}
|
||||
@@ -97,12 +87,12 @@ function Tools() {
|
||||
loading={loading.cloud}
|
||||
onClick={() => {
|
||||
// Store parameters
|
||||
const params = { ...systemConfig.backup.params };
|
||||
const params = { ...fileSystemParams };
|
||||
params[fileSystemType] = fileSystemParams;
|
||||
systemConfig.backup = {
|
||||
systemConfig.setBackup({
|
||||
filesystem: fileSystemType,
|
||||
params,
|
||||
};
|
||||
});
|
||||
setLoading((prev) => ({ ...prev, cloud: true }));
|
||||
Message.info(t("preparing_backup")!);
|
||||
syncCtrl
|
||||
@@ -124,10 +114,7 @@ function Tools() {
|
||||
key="list"
|
||||
type="primary"
|
||||
onClick={async () => {
|
||||
let fs = await FileSystemFactory.create(
|
||||
fileSystemType,
|
||||
fileSystemParams
|
||||
);
|
||||
let fs = await FileSystemFactory.create(fileSystemType, fileSystemParams);
|
||||
try {
|
||||
fs = await fs.openDir("ScriptCat");
|
||||
let list = await fs.list();
|
||||
@@ -159,10 +146,7 @@ function Tools() {
|
||||
type="secondary"
|
||||
size="mini"
|
||||
onClick={async () => {
|
||||
let fs = await FileSystemFactory.create(
|
||||
fileSystemType,
|
||||
fileSystemParams
|
||||
);
|
||||
let fs = await FileSystemFactory.create(fileSystemType, fileSystemParams);
|
||||
try {
|
||||
fs = await fs.openDir("ScriptCat");
|
||||
const url = await fs.getDirUrl();
|
||||
@@ -191,20 +175,14 @@ function Tools() {
|
||||
dataSource={backupFileList}
|
||||
render={(item: File) => (
|
||||
<List.Item key={item.name}>
|
||||
<List.Item.Meta
|
||||
title={item.name}
|
||||
description={formatUnixTime(item.updatetime / 1000)}
|
||||
/>
|
||||
<List.Item.Meta title={item.name} description={formatUnixTime(item.updatetime / 1000)} />
|
||||
<Space className="w-full justify-end">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
Message.info(t("pulling_data_from_cloud")!);
|
||||
let fs = await FileSystemFactory.create(
|
||||
fileSystemType,
|
||||
fileSystemParams
|
||||
);
|
||||
let fs = await FileSystemFactory.create(fileSystemType, fileSystemParams);
|
||||
let file: FileReader;
|
||||
let data: Blob;
|
||||
try {
|
||||
@@ -238,22 +216,13 @@ function Tools() {
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
title: t("confirm_delete"),
|
||||
content: `${t("confirm_delete_backup_file")}${
|
||||
item.name
|
||||
}?`,
|
||||
content: `${t("confirm_delete_backup_file")}${item.name}?`,
|
||||
onOk: async () => {
|
||||
let fs = await FileSystemFactory.create(
|
||||
fileSystemType,
|
||||
fileSystemParams
|
||||
);
|
||||
let fs = await FileSystemFactory.create(fileSystemType, fileSystemParams);
|
||||
try {
|
||||
fs = await fs.openDir("ScriptCat");
|
||||
await fs.delete(item.name);
|
||||
setBackupFileList(
|
||||
backupFileList.filter(
|
||||
(i) => i.name !== item.name
|
||||
)
|
||||
);
|
||||
setBackupFileList(backupFileList.filter((i) => i.name !== item.name));
|
||||
Message.success(t("delete_success")!);
|
||||
} catch (e) {
|
||||
Message.error(`${t("delete_failed")}${e}`);
|
||||
|
@@ -129,7 +129,7 @@ const emptyScript = async (template: string, hotKeys: any, target?: string) => {
|
||||
|
||||
return Promise.resolve({
|
||||
script,
|
||||
code: script.code,
|
||||
code,
|
||||
active: true,
|
||||
hotKeys,
|
||||
isChanged: false,
|
||||
@@ -218,7 +218,7 @@ function ScriptEditor() {
|
||||
setEditors((prev) => {
|
||||
for (let i = 0; i < prev.length; i += 1) {
|
||||
if (prev[i].script.uuid === newScript.uuid) {
|
||||
prev[i].script.code = newScript.code;
|
||||
prev[i].script.code = prepareScript.scriptCode;
|
||||
prev[i].isChanged = false;
|
||||
prev[i].script.name = newScript.name;
|
||||
break;
|
||||
@@ -777,14 +777,13 @@ function ScriptEditor() {
|
||||
setEditors((prev) => {
|
||||
const i = parseInt(index, 10);
|
||||
if (prev[i].isChanged) {
|
||||
// eslint-disable-next-line no-restricted-globals, no-alert
|
||||
if (!confirm("脚本已修改, 关闭后会丢失修改, 是否继续?")) {
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
if (prev.length === 1) {
|
||||
// 如果是id打开的回退到列表
|
||||
if (id) {
|
||||
// 如果是uuid打开的回退到列表
|
||||
if (uuid) {
|
||||
navigate("/");
|
||||
return prev;
|
||||
}
|
||||
@@ -801,7 +800,7 @@ function ScriptEditor() {
|
||||
setSelectSciptButtonAndTab(prev[i - 1].script.uuid);
|
||||
} else {
|
||||
prev[i + 1].active = true;
|
||||
setSelectSciptButtonAndTab(prev[i - 1].script.uuid);
|
||||
setSelectSciptButtonAndTab(prev[i + 1].script.uuid);
|
||||
}
|
||||
}
|
||||
prev.splice(i, 1);
|
||||
|
@@ -147,10 +147,6 @@ export function ListHomeRender({ script }: { script: Script }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getValues(script: Script) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
export type ScriptIconsProps = {
|
||||
script: { name: string; metadata: Metadata };
|
||||
size?: number;
|
||||
|
51
src/pages/store/features/config.ts
Normal file
51
src/pages/store/features/config.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { SystemConfig } from "@App/pkg/config/config";
|
||||
import { createAppSlice } from "../hooks";
|
||||
import { PayloadAction } from "@reduxjs/toolkit";
|
||||
import { editor } from "monaco-editor";
|
||||
|
||||
function setAutoMode() {
|
||||
const darkTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const isMatch = (match: boolean) => {
|
||||
if (match) {
|
||||
document.body.setAttribute("arco-theme", "dark");
|
||||
editor.setTheme("vs-dark");
|
||||
} else {
|
||||
document.body.removeAttribute("arco-theme");
|
||||
editor.setTheme("vs");
|
||||
}
|
||||
};
|
||||
darkTheme.addEventListener("change", (e) => {
|
||||
isMatch(e.matches);
|
||||
});
|
||||
isMatch(darkTheme.matches);
|
||||
}
|
||||
|
||||
export const configSlice = createAppSlice({
|
||||
name: "setting",
|
||||
initialState: {
|
||||
lightMode: localStorage.lightMode || "auto",
|
||||
},
|
||||
reducers: (create) => {
|
||||
// 初始化黑夜模式
|
||||
setAutoMode();
|
||||
return {
|
||||
setDarkMode: create.reducer((state, action: PayloadAction<"light" | "dark" | "auto">) => {
|
||||
localStorage.loghtMode = action.payload;
|
||||
state.lightMode = action.payload;
|
||||
if (action.payload === "auto") {
|
||||
setAutoMode();
|
||||
} else {
|
||||
document.body.setAttribute("arco-theme", action.payload);
|
||||
editor.setTheme(action.payload === "dark" ? "vs-dark" : "vs");
|
||||
}
|
||||
}),
|
||||
};
|
||||
},
|
||||
selectors: {
|
||||
selectThemeMode: (state) => state.lightMode,
|
||||
},
|
||||
});
|
||||
|
||||
export const { setDarkMode } = configSlice.actions;
|
||||
|
||||
export const { selectThemeMode } = configSlice.selectors;
|
@@ -8,12 +8,13 @@ import {
|
||||
ScriptDAO,
|
||||
} from "@App/app/repo/scripts";
|
||||
import { arrayMove } from "@dnd-kit/sortable";
|
||||
import { PopupClient, RuntimeClient, ScriptClient } from "@App/app/service/service_worker/client";
|
||||
import { PermissionClient, PopupClient, RuntimeClient, ScriptClient } from "@App/app/service/service_worker/client";
|
||||
import { message } from "../global";
|
||||
|
||||
export const scriptClient = new ScriptClient(message);
|
||||
export const runtimeClient = new RuntimeClient(message);
|
||||
export const popupClient = new PopupClient(message);
|
||||
export const permissionClient = new PermissionClient(message);
|
||||
|
||||
export const fetchAndSortScriptList = createAsyncThunk("script/fetchScriptList", async () => {
|
||||
// 排序
|
||||
|
@@ -1,60 +0,0 @@
|
||||
import { createAppSlice } from "../hooks";
|
||||
import { PayloadAction } from "@reduxjs/toolkit";
|
||||
import { editor } from "monaco-editor";
|
||||
|
||||
export const settingSlice = createAppSlice({
|
||||
name: "setting",
|
||||
initialState: {
|
||||
lightMode: localStorage.lightMode || "auto",
|
||||
eslint: {
|
||||
enable: true,
|
||||
config: "",
|
||||
},
|
||||
scriptListColumnWidth: {} as { [key: string]: number },
|
||||
menuExpandNum: 5,
|
||||
},
|
||||
reducers: (create) => {
|
||||
// 初始化黑夜模式
|
||||
const setAutoMode = () => {
|
||||
const darkTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const isMatch = (match: boolean) => {
|
||||
if (match) {
|
||||
document.body.setAttribute("arco-theme", "dark");
|
||||
editor.setTheme("vs-dark");
|
||||
} else {
|
||||
document.body.removeAttribute("arco-theme");
|
||||
editor.setTheme("vs");
|
||||
}
|
||||
};
|
||||
darkTheme.addEventListener("change", (e) => {
|
||||
isMatch(e.matches);
|
||||
});
|
||||
isMatch(darkTheme.matches);
|
||||
};
|
||||
setAutoMode();
|
||||
return {
|
||||
setDarkMode: create.reducer((state, action: PayloadAction<"light" | "dark" | "auto">) => {
|
||||
localStorage.loghtMode = action.payload;
|
||||
state.lightMode = action.payload;
|
||||
if (action.payload === "auto") {
|
||||
setAutoMode();
|
||||
} else {
|
||||
document.body.setAttribute("arco-theme", action.payload);
|
||||
editor.setTheme(action.payload === "dark" ? "vs-dark" : "vs");
|
||||
}
|
||||
}),
|
||||
menuExpandNum: create.reducer((state, action: PayloadAction<number>) => {
|
||||
state.menuExpandNum = action.payload;
|
||||
}),
|
||||
};
|
||||
},
|
||||
selectors: {
|
||||
selectThemeMode: (state) => state.lightMode,
|
||||
selectScriptListColumnWidth: (state) => state.scriptListColumnWidth,
|
||||
selectMenuExpandNum: (state) => state.menuExpandNum,
|
||||
},
|
||||
});
|
||||
|
||||
export const { setDarkMode } = settingSlice.actions;
|
||||
|
||||
export const { selectThemeMode, selectScriptListColumnWidth, selectMenuExpandNum } = settingSlice.selectors;
|
@@ -1,5 +1,8 @@
|
||||
import { SystemConfig } from "@App/pkg/config/config";
|
||||
import { ExtensionMessage } from "@Packages/message/extension_message";
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
|
||||
export const message = new ExtensionMessage();
|
||||
export const messageQueue = new MessageQueue();
|
||||
export const systemConfig = new SystemConfig(messageQueue);
|
||||
export const globalCache = new Map<string, any>();
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import type { Action, ThunkAction } from "@reduxjs/toolkit";
|
||||
import { combineSlices, configureStore } from "@reduxjs/toolkit";
|
||||
import { setupListeners } from "@reduxjs/toolkit/query";
|
||||
import { settingSlice } from "./features/setting";
|
||||
import { scriptSlice } from "./features/script";
|
||||
import { configSlice } from "./features/config";
|
||||
|
||||
// `combineSlices` automatically combines the reducers using
|
||||
// their `reducerPath`s, therefore we no longer need to call `combineReducers`.
|
||||
const rootReducer = combineSlices(settingSlice, scriptSlice);
|
||||
const rootReducer = combineSlices(configSlice, scriptSlice);
|
||||
// Infer the `RootState` type from the root reducer
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
|
||||
|
@@ -7,6 +7,7 @@ export default function storeSubscribe() {
|
||||
subscribeScriptRunStatus(messageQueue, (data) => {
|
||||
store.dispatch(scriptSlice.actions.updateRunStatus(data));
|
||||
});
|
||||
|
||||
subscribeScriptInstall(messageQueue, (message) => {
|
||||
store.dispatch(upsertScript(message.script));
|
||||
});
|
||||
|
58
src/pkg/config/chrome_storage.ts
Normal file
58
src/pkg/config/chrome_storage.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
export default class ChromeStorage {
|
||||
private prefix: string;
|
||||
|
||||
private storage: chrome.storage.StorageArea;
|
||||
|
||||
constructor(prefix: string, sync: boolean) {
|
||||
this.prefix = `${prefix}_`;
|
||||
this.storage = sync ? chrome.storage.sync : chrome.storage.local;
|
||||
}
|
||||
|
||||
public buildKey(key: string): string {
|
||||
return this.prefix + key;
|
||||
}
|
||||
|
||||
public get(key: string): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
key = this.buildKey(key);
|
||||
this.storage.get(key, (items) => {
|
||||
resolve(items[key]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public set(key: string, value: any): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const kvp: { [key: string]: any } = {};
|
||||
kvp[this.buildKey(key)] = value;
|
||||
this.storage.set(kvp, () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
public remove(key: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.storage.remove(this.buildKey(key), () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
public removeAll(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.storage.clear(() => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
public keys(): Promise<{ [key: string]: any }> {
|
||||
return new Promise((resolve) => {
|
||||
const ret: { [key: string]: any } = {};
|
||||
const prefix = this.buildKey("");
|
||||
this.storage.get((items) => {
|
||||
Object.keys(items).forEach((key) => {
|
||||
if (key.startsWith(prefix)) {
|
||||
ret[key.substring(prefix.length)] = items[key];
|
||||
}
|
||||
});
|
||||
resolve(ret);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
241
src/pkg/config/config.ts
Normal file
241
src/pkg/config/config.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { Message } from "@arco-design/web-react";
|
||||
import ChromeStorage from "./chrome_storage";
|
||||
import { defaultConfig } from "../../../packages/eslint/linter-config";
|
||||
import { FileSystemType } from "@Packages/filesystem/factory";
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import i18n from "@App/locales/locales";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const SystamConfigChange = "systemConfigChange";
|
||||
|
||||
export type CloudSyncConfig = {
|
||||
enable: boolean;
|
||||
syncDelete: boolean;
|
||||
filesystem: FileSystemType;
|
||||
params: { [key: string]: any };
|
||||
};
|
||||
|
||||
export type CATFileStorage = {
|
||||
filesystem: FileSystemType;
|
||||
params: { [key: string]: any };
|
||||
status: "unset" | "success" | "error";
|
||||
};
|
||||
|
||||
export class SystemConfig {
|
||||
public cache = new Map<string, any>();
|
||||
|
||||
public storage = new ChromeStorage("system", true);
|
||||
|
||||
constructor(private mq: MessageQueue) {
|
||||
this.mq.subscribe("systemConfigChange", (msg) => {
|
||||
const { key, value } = msg;
|
||||
this.cache.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
async getAll(): Promise<{ [key: string]: any }> {
|
||||
const ret: { [key: string]: any } = {};
|
||||
const list = await this.storage.keys();
|
||||
Object.keys(list).forEach((key) => {
|
||||
this.cache.set(key, list[key]);
|
||||
ret[key] = list[key];
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
get<T>(key: string, defaultValue: T): Promise<T> {
|
||||
if (this.cache.has(key)) {
|
||||
return Promise.resolve(this.cache.get(key));
|
||||
}
|
||||
return this.storage.get(key).then((val) => {
|
||||
if (val === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
this.cache.set(key, val);
|
||||
return val;
|
||||
});
|
||||
}
|
||||
|
||||
public set(key: string, val: any) {
|
||||
this.cache.set(key, val);
|
||||
this.storage.set(key, val);
|
||||
// 发送消息通知更新
|
||||
this.mq.publish(SystamConfigChange, {
|
||||
key,
|
||||
value: val,
|
||||
});
|
||||
}
|
||||
|
||||
public getChangetime() {
|
||||
return this.get("changetime", 0);
|
||||
}
|
||||
|
||||
public setChangetime(n: number) {
|
||||
this.set("changetime", 0);
|
||||
}
|
||||
|
||||
// 检查更新周期,单位为秒
|
||||
public getCheckScriptUpdateCycle() {
|
||||
return this.get("check_script_update_cycle", 86400);
|
||||
}
|
||||
|
||||
public setCheckScriptUpdateCycle(n: number) {
|
||||
this.set("check_script_update_cycle", n);
|
||||
}
|
||||
|
||||
public getSilenceUpdateScript() {
|
||||
return this.get("silence_update_script", false);
|
||||
}
|
||||
|
||||
public setSilenceUpdateScript(val: boolean) {
|
||||
this.set("silence_update_script", val);
|
||||
}
|
||||
|
||||
public getEnableAutoSync() {
|
||||
return this.get("enable_auto_sync", true);
|
||||
}
|
||||
|
||||
public setEnableAutoSync(enable: boolean) {
|
||||
this.set("enable_auto_sync", enable);
|
||||
}
|
||||
|
||||
// 更新已经禁用的脚本
|
||||
public getUpdateDisableScript() {
|
||||
return this.get("update_disable_script", true);
|
||||
}
|
||||
|
||||
public setUpdateDisableScript(enable: boolean) {
|
||||
this.set("update_disable_script", enable);
|
||||
}
|
||||
|
||||
public getVscodeUrl() {
|
||||
return this.get("vscode_url", "ws://localhost:8642");
|
||||
}
|
||||
|
||||
public setVscodeUrl(val: string) {
|
||||
this.set("vscode_url", val);
|
||||
}
|
||||
|
||||
public getVscodeReconnect() {
|
||||
return this.get("vscode_reconnect", false);
|
||||
}
|
||||
|
||||
public setVscodeReconnect(val: boolean) {
|
||||
this.set("vscode_reconnect", val);
|
||||
}
|
||||
|
||||
public getBackup(): Promise<{
|
||||
filesystem: FileSystemType;
|
||||
params: { [key: string]: any };
|
||||
}> {
|
||||
return this.get("backup", {
|
||||
filesystem: "webdav",
|
||||
params: {},
|
||||
});
|
||||
}
|
||||
|
||||
public setBackup(data: { filesystem: FileSystemType; params: { [key: string]: any } }) {
|
||||
this.set("backup", data);
|
||||
}
|
||||
|
||||
getCloudSync(): Promise<CloudSyncConfig> {
|
||||
return this.get("cloud_sync", {
|
||||
enable: false,
|
||||
syncDelete: true,
|
||||
filesystem: "webdav",
|
||||
params: {},
|
||||
});
|
||||
}
|
||||
|
||||
setCloudSync(data: CloudSyncConfig) {
|
||||
this.set("cloud_sync", data);
|
||||
}
|
||||
|
||||
getCatFileStorage(): Promise<CATFileStorage> {
|
||||
return this.get("cat_file_storage", {
|
||||
status: "unset",
|
||||
filesystem: "webdav",
|
||||
params: {},
|
||||
});
|
||||
}
|
||||
|
||||
setCatFileStorage(data: CATFileStorage | undefined) {
|
||||
this.set("cat_file_storage", data);
|
||||
}
|
||||
|
||||
getEnableEslint() {
|
||||
return this.get("enable_eslint", true);
|
||||
}
|
||||
|
||||
setEnableEslint(val: boolean) {
|
||||
this.set("enable_eslint", val);
|
||||
}
|
||||
|
||||
getEslintConfig() {
|
||||
return this.get("eslint_config", defaultConfig);
|
||||
}
|
||||
|
||||
setEslintConfig(v: string) {
|
||||
if (v === "") {
|
||||
this.set("eslint_config", v);
|
||||
Message.success("ESLint规则已重置");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSON.parse(v);
|
||||
this.set("eslint_config", v);
|
||||
Message.success("ESLint规则已保存");
|
||||
} catch (err: any) {
|
||||
Message.error(err.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// 日志清理周期
|
||||
getLogCleanCycle() {
|
||||
return this.get("log_clean_cycle", 7);
|
||||
}
|
||||
|
||||
setLogCleanCycle(val: number) {
|
||||
this.set("log_clean_cycle", val);
|
||||
}
|
||||
|
||||
// 设置脚本列表列宽度
|
||||
getScriptListColumnWidth() {
|
||||
return this.get<{ [key: string]: number }>("script_list_column_width", {});
|
||||
}
|
||||
|
||||
setScriptListColumnWidth(val: { [key: string]: number }) {
|
||||
this.set("script_list_column_width", val);
|
||||
}
|
||||
|
||||
// 展开菜单数
|
||||
getMenuExpandNum() {
|
||||
return this.get("menu_expand_num", 5);
|
||||
}
|
||||
|
||||
setMenuExpandNum(val: number) {
|
||||
this.set("menu_expand_num", val);
|
||||
}
|
||||
|
||||
async getLanguage() {
|
||||
const defaultLanguage = await new Promise<string>((resolve) => {
|
||||
chrome.i18n.getAcceptLanguages((lngs) => {
|
||||
// 遍历数组寻找匹配语言
|
||||
for (let i = 0; i < lngs.length; i += 1) {
|
||||
const lng = lngs[i];
|
||||
if (i18n.hasResourceBundle(lng, "translation")) {
|
||||
resolve(lng);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return this.get("language", defaultLanguage || chrome.i18n.getUILanguage());
|
||||
}
|
||||
|
||||
setLanguage(value: any) {
|
||||
this.set("language", value);
|
||||
i18n.changeLanguage(value);
|
||||
dayjs.locale(value.toLocaleLowerCase());
|
||||
}
|
||||
}
|
@@ -43,10 +43,10 @@ describe("UrlMatch-google-error", () => {
|
||||
url.add("https://*foo/bar", "ok1");
|
||||
}).toThrow(Error);
|
||||
});
|
||||
// 从v0.17.0开始允许这种
|
||||
it("error-2", () => {
|
||||
expect(() => {
|
||||
url.add("https://foo.*.bar/baz", "ok1");
|
||||
}).toThrow(Error);
|
||||
url.add("https://foo.*.bar/baz", "ok1");
|
||||
expect(url.match("https://foo.api.bar/baz")).toEqual(["ok1"]);
|
||||
});
|
||||
it("error-3", () => {
|
||||
expect(() => {
|
||||
@@ -70,6 +70,13 @@ describe("UrlMatch-search", () => {
|
||||
expect(url.match("https://bbs.tampermonkey.net.cn/")).toEqual(["ok2"]);
|
||||
expect(url.match("https://bbs.tampermonkey.net.cn/foo/bar.html")).toEqual([]);
|
||||
});
|
||||
it("http://api.*.example.com/*", () => {
|
||||
const url = new UrlMatch<string>();
|
||||
url.add("http://api.*.example.com/*", "ok1");
|
||||
expect(url.match("http://api.foo.example.com/")).toEqual(["ok1"]);
|
||||
expect(url.match("http://api.bar.example.com/")).toEqual(["ok1"]);
|
||||
expect(url.match("http://api.example.com/")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("UrlMatch-port1", () => {
|
||||
@@ -106,13 +113,47 @@ describe("UrlMatch-port2", () => {
|
||||
// https://developer.chrome.com/docs/extensions/mv3/match_patterns/
|
||||
describe("dealPatternMatches", () => {
|
||||
it("https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn#examples", () => {
|
||||
const matches = dealPatternMatches(["https://*/*", "http://127.0.0.1/*", "http://127.0.0.1/"]);
|
||||
expect(matches.patternResult).toEqual(["https://*/*", "http://127.0.0.1/*", "http://127.0.0.1/"]);
|
||||
const matches = dealPatternMatches([
|
||||
"https://*/*",
|
||||
"http://127.0.0.1/*",
|
||||
"http://127.0.0.1/",
|
||||
"https://*.example.com/*",
|
||||
]);
|
||||
expect(matches.patternResult).toEqual([
|
||||
"https://*/*",
|
||||
"http://127.0.0.1/*",
|
||||
"http://127.0.0.1/",
|
||||
"https://*.example.com/*",
|
||||
]);
|
||||
});
|
||||
// 处理一些特殊情况
|
||||
it("*://link.17173.com*", () => {
|
||||
const matches = dealPatternMatches(["*://link.17173.com*"]);
|
||||
expect(matches.patternResult).toEqual(["*://link.17173.com/*"]);
|
||||
it("特殊情况", () => {
|
||||
const matches = dealPatternMatches([
|
||||
"*://www.example.com*",
|
||||
"*://api.*.example.com/*",
|
||||
"*://api.*.*.example.com/*",
|
||||
"*://*example.com/*",
|
||||
]);
|
||||
expect(matches.patternResult).toEqual([
|
||||
"*://www.example.com/*",
|
||||
"*://*.example.com/*",
|
||||
"*://*.example.com/*",
|
||||
"*://example.com/*",
|
||||
]);
|
||||
expect(matches.result).toEqual([
|
||||
"*://www.example.com*",
|
||||
"*://api.*.example.com/*",
|
||||
"*://api.*.*.example.com/*",
|
||||
"*://*example.com/*",
|
||||
]);
|
||||
});
|
||||
it("特殊情况-exclude", () => {
|
||||
const matches = dealPatternMatches(["*://api.*.example.com/*", "*://api.*.*.example.com/*"], {
|
||||
exclude: true,
|
||||
});
|
||||
console.log(matches);
|
||||
expect(matches.patternResult).toEqual(["*://example.com/*", "*://example.com/*"]);
|
||||
expect(matches.result).toEqual(["*://api.*.example.com/*", "*://api.*.*.example.com/*"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,11 +187,19 @@ describe("parsePatternMatchesURL", () => {
|
||||
path: "*",
|
||||
});
|
||||
});
|
||||
it("*://link.17173.com*", () => {
|
||||
const matches = parsePatternMatchesURL("*://link.17173.com*");
|
||||
it("*://www.example.com*", () => {
|
||||
const matches = parsePatternMatchesURL("*://www.example.com*");
|
||||
expect(matches).toEqual({
|
||||
scheme: "*",
|
||||
host: "link.17173.com",
|
||||
host: "www.example.com",
|
||||
path: "*",
|
||||
});
|
||||
});
|
||||
it("*://api.*.example.com/*", () => {
|
||||
const matches = parsePatternMatchesURL("*://api.*.example.com/*");
|
||||
expect(matches).toEqual({
|
||||
scheme: "*",
|
||||
host: "*.example.com",
|
||||
path: "*",
|
||||
});
|
||||
});
|
||||
|
@@ -69,8 +69,6 @@ export default class Match<T> {
|
||||
if (!u.host.endsWith(":*")) {
|
||||
u.host = u.host.substring(0, u.host.length - 1);
|
||||
}
|
||||
} else if (pos !== -1 && pos !== 0) {
|
||||
return "";
|
||||
}
|
||||
u.host = u.host.replace(/\*/g, "[^/]*?");
|
||||
// 处理 *.开头
|
||||
@@ -225,7 +223,12 @@ export interface PatternMatchesUrl {
|
||||
}
|
||||
|
||||
// 解析URL, 根据https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn进行处理
|
||||
export function parsePatternMatchesURL(url: string): PatternMatchesUrl | undefined {
|
||||
export function parsePatternMatchesURL(
|
||||
url: string,
|
||||
options?: {
|
||||
exclude?: boolean;
|
||||
}
|
||||
): PatternMatchesUrl | undefined {
|
||||
let result: PatternMatchesUrl | undefined;
|
||||
const match = /^(.+?):\/\/(.*?)(\/(.*?)(\?.*?|)|)$/.exec(url);
|
||||
if (match) {
|
||||
@@ -260,17 +263,38 @@ export function parsePatternMatchesURL(url: string): PatternMatchesUrl | undefin
|
||||
if (result.host.endsWith("*")) {
|
||||
result.host = result.host.slice(0, -1);
|
||||
}
|
||||
// 处理 www.*.example.com 的情况为 *.example.com
|
||||
const pos = result.host.lastIndexOf("*");
|
||||
if (pos > 0 && pos < result.host.length - 1) {
|
||||
if (options && options.exclude) {
|
||||
// 如果是exclude, 按最小匹配处理
|
||||
// 包括*也去掉
|
||||
result.host = result.host.substring(pos + 1);
|
||||
if (result.host.startsWith(".")) {
|
||||
result.host = result.host.substring(1);
|
||||
}
|
||||
} else {
|
||||
// 如果不是exclude
|
||||
// 将*前面的全部去掉
|
||||
result.host = result.host.substring(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 处理油猴的match和include为chrome的pattern-matche
|
||||
export function dealPatternMatches(matches: string[]) {
|
||||
export function dealPatternMatches(
|
||||
matches: string[],
|
||||
options?: {
|
||||
exclude?: boolean;
|
||||
}
|
||||
) {
|
||||
const patternResult: string[] = [];
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
const url = parsePatternMatchesURL(matches[i]);
|
||||
const url = parsePatternMatchesURL(matches[i], options);
|
||||
if (url) {
|
||||
patternResult.push(`${url.scheme}://${url.host}/${url.path}`);
|
||||
result.push(matches[i]);
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import dts from "@App/types/scriptcat.d.ts";
|
||||
import { globalCache } from "@App/pages/store/global";
|
||||
import dts from "@App/template/scriptcat.d.tpl";
|
||||
import EventEmitter from "eventemitter3";
|
||||
import { languages } from "monaco-editor";
|
||||
|
||||
// 注册eslint
|
||||
// const linterWorker = new Worker("/src/linter.worker.js");
|
||||
const linterWorker = new Worker("/src/linter.worker.js");
|
||||
|
||||
export default function registerEditor() {
|
||||
window.MonacoEnvironment = {
|
||||
@@ -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 } = {
|
||||
@@ -59,74 +61,74 @@ export default function registerEditor() {
|
||||
},
|
||||
});
|
||||
|
||||
// // 处理quick fix
|
||||
// languages.registerCodeActionProvider("javascript", {
|
||||
// provideCodeActions: (model /** ITextModel */, range /** Range */, context /** CodeActionContext */) => {
|
||||
// // const actions: languages.CodeAction[] = [];
|
||||
// // const eslintFix = <Map<string, any>>Cache.getInstance().get("eslint-fix");
|
||||
// // for (let i = 0; i < context.markers.length; i += 1) {
|
||||
// // // 判断有没有修复方案
|
||||
// // const val = context.markers[i];
|
||||
// // const code = typeof val.code === "string" ? val.code : val.code!.value;
|
||||
// // const fix = eslintFix.get(
|
||||
// // `${code}|${val.startLineNumber}|${val.endLineNumber}|${val.startColumn}|${val.endColumn}`
|
||||
// // );
|
||||
// // if (fix) {
|
||||
// // const edit: languages.IWorkspaceTextEdit = {
|
||||
// // resource: model.uri,
|
||||
// // textEdit: {
|
||||
// // range: fix.range,
|
||||
// // text: fix.text,
|
||||
// // },
|
||||
// // versionId: undefined,
|
||||
// // };
|
||||
// // actions.push(<languages.CodeAction>{
|
||||
// // title: `修复 ${code} 问题`,
|
||||
// // diagnostics: [val],
|
||||
// // kind: "quickfix",
|
||||
// // edit: {
|
||||
// // edits: [edit],
|
||||
// // },
|
||||
// // isPreferred: true,
|
||||
// // });
|
||||
// // }
|
||||
// // }
|
||||
// 处理quick fix
|
||||
languages.registerCodeActionProvider("javascript", {
|
||||
provideCodeActions: (model /** ITextModel */, range /** Range */, context /** CodeActionContext */) => {
|
||||
const actions: languages.CodeAction[] = [];
|
||||
const eslintFix = <Map<string, any>>globalCache.get("eslint-fix");
|
||||
for (let i = 0; i < context.markers.length; i += 1) {
|
||||
// 判断有没有修复方案
|
||||
const val = context.markers[i];
|
||||
const code = typeof val.code === "string" ? val.code : val.code!.value;
|
||||
const fix = eslintFix.get(
|
||||
`${code}|${val.startLineNumber}|${val.endLineNumber}|${val.startColumn}|${val.endColumn}`
|
||||
);
|
||||
if (fix) {
|
||||
const edit: languages.IWorkspaceTextEdit = {
|
||||
resource: model.uri,
|
||||
textEdit: {
|
||||
range: fix.range,
|
||||
text: fix.text,
|
||||
},
|
||||
versionId: undefined,
|
||||
};
|
||||
actions.push(<languages.CodeAction>{
|
||||
title: `修复 ${code} 问题`,
|
||||
diagnostics: [val],
|
||||
kind: "quickfix",
|
||||
edit: {
|
||||
edits: [edit],
|
||||
},
|
||||
isPreferred: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// // const actions = context.markers.map((error) => {
|
||||
// // const edit: languages.IWorkspaceTextEdit = {
|
||||
// // resource: model.uri,
|
||||
// // textEdit: {
|
||||
// // range,
|
||||
// // text: "console.log(1)",
|
||||
// // },
|
||||
// // versionId: undefined,
|
||||
// // };
|
||||
// // return <languages.CodeAction>{
|
||||
// // title: ``,
|
||||
// // diagnostics: [error],
|
||||
// // kind: "quickfix",
|
||||
// // edit: {
|
||||
// // edits: [edit],
|
||||
// // },
|
||||
// // isPreferred: true,
|
||||
// // };
|
||||
// // });
|
||||
// return {
|
||||
// actions,
|
||||
// dispose: () => {},
|
||||
// };
|
||||
// },
|
||||
// });
|
||||
// const actions = context.markers.map((error) => {
|
||||
// const edit: languages.IWorkspaceTextEdit = {
|
||||
// resource: model.uri,
|
||||
// textEdit: {
|
||||
// range,
|
||||
// text: "console.log(1)",
|
||||
// },
|
||||
// versionId: undefined,
|
||||
// };
|
||||
// return <languages.CodeAction>{
|
||||
// title: ``,
|
||||
// diagnostics: [error],
|
||||
// kind: "quickfix",
|
||||
// edit: {
|
||||
// edits: [edit],
|
||||
// },
|
||||
// isPreferred: true,
|
||||
// };
|
||||
// });
|
||||
return {
|
||||
actions,
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// export class LinterWorker {
|
||||
// static hook = new EventEmitter();
|
||||
export class LinterWorker {
|
||||
static hook = new EventEmitter();
|
||||
|
||||
// static sendLinterMessage(data: unknown) {
|
||||
// linterWorker.postMessage(data);
|
||||
// }
|
||||
// }
|
||||
static sendLinterMessage(data: unknown) {
|
||||
linterWorker.postMessage(data);
|
||||
}
|
||||
}
|
||||
|
||||
// linterWorker.onmessage = (event) => {
|
||||
// LinterWorker.hook.emit("message", event.data);
|
||||
// };
|
||||
linterWorker.onmessage = (event) => {
|
||||
LinterWorker.hook.emit("message", event.data);
|
||||
};
|
||||
|
@@ -139,7 +139,7 @@ export async function fetchScriptInfo(
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function copyScript(script: ScriptAndCode, old: Script): ScriptAndCode {
|
||||
export function copyScript(script: Script, old: Script): Script {
|
||||
const ret = script;
|
||||
ret.uuid = old.uuid;
|
||||
ret.createtime = old.createtime;
|
||||
@@ -206,7 +206,7 @@ export function prepareScriptByCode(
|
||||
url: string,
|
||||
uuid?: string,
|
||||
override?: boolean
|
||||
): Promise<{ script: ScriptAndCode; oldScript?: ScriptAndCode }> {
|
||||
): Promise<{ script: Script; oldScript?: Script; oldScriptCode?: string }> {
|
||||
const dao = new ScriptDAO();
|
||||
return new Promise((resolve, reject) => {
|
||||
const metadata = parseMetadata(code);
|
||||
@@ -255,10 +255,9 @@ export function prepareScriptByCode(
|
||||
} else {
|
||||
newUUID = uuidv4();
|
||||
}
|
||||
let script: ScriptAndCode = {
|
||||
let script: Script = {
|
||||
uuid: newUUID,
|
||||
name: metadata.name[0],
|
||||
code: code,
|
||||
author: metadata.author && metadata.author[0],
|
||||
namespace: metadata.namespace && metadata.namespace[0],
|
||||
originDomain: domain,
|
||||
@@ -309,7 +308,7 @@ export function prepareScriptByCode(
|
||||
}
|
||||
script.checktime = new Date().getTime();
|
||||
}
|
||||
resolve({ script, oldScript: old ? Object.assign(old, oldCode) : undefined });
|
||||
resolve({ script, oldScript: old, oldScriptCode: oldCode?.code });
|
||||
};
|
||||
handler();
|
||||
});
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import { Metadata } from "@App/app/repo/scripts";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import { Metadata, Script } from "@App/app/repo/scripts";
|
||||
import { CronTime } from "cron";
|
||||
import crypto from "crypto-js";
|
||||
import dayjs from "dayjs";
|
||||
import semver from "semver";
|
||||
|
||||
@@ -143,12 +145,12 @@ export function parseStorageValue(str: string): unknown {
|
||||
}
|
||||
|
||||
// 对比版本大小
|
||||
export function ltever(newVersion: string, oldVersion: string) {
|
||||
export function ltever(newVersion: string, oldVersion: string, logger?: Logger) {
|
||||
// 先验证符不符合语义化版本规范
|
||||
try {
|
||||
return semver.lte(newVersion, oldVersion);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
logger?.warn("does not conform to the Semantic Versioning specification", Logger.E(e));
|
||||
}
|
||||
const newVer = newVersion.split(".");
|
||||
const oldVer = oldVersion.split(".");
|
||||
@@ -217,3 +219,35 @@ 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;
|
||||
}
|
||||
|
||||
export function getIcon(script: Script): string | undefined {
|
||||
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])
|
||||
);
|
||||
}
|
||||
|
||||
export function calculateMd5(blob: Blob) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(blob);
|
||||
reader.onloadend = () => {
|
||||
if (!reader.result) {
|
||||
reject(new Error("result is null"));
|
||||
} else {
|
||||
const wordArray = crypto.lib.WordArray.create(<ArrayBuffer>reader.result);
|
||||
resolve(crypto.MD5(wordArray).toString());
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@@ -1,735 +0,0 @@
|
||||
// 脚本运行时,主要负责脚本的加载和匹配
|
||||
// 油猴脚本将监听页面的创建,将代码注入到页面中
|
||||
import MessageSandbox from "@App/app/message/sandbox";
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import {
|
||||
Script,
|
||||
SCRIPT_RUN_STATUS,
|
||||
SCRIPT_STATUS_ENABLE,
|
||||
SCRIPT_TYPE_NORMAL,
|
||||
ScriptDAO,
|
||||
ScriptRunResouce,
|
||||
SCRIPT_RUN_STATUS_RUNNING,
|
||||
Metadata,
|
||||
} from "@App/app/repo/scripts";
|
||||
import ResourceManager from "@App/app/service/resource/manager";
|
||||
import ValueManager from "@App/app/service/value/manager";
|
||||
import { dealScript, randomString } from "@App/pkg/utils/utils";
|
||||
import { UrlInclude, UrlMatch } from "@App/pkg/utils/match";
|
||||
import {
|
||||
MessageHander,
|
||||
MessageSender,
|
||||
TargetTag,
|
||||
} from "@App/app/message/message";
|
||||
import ScriptManager from "@App/app/service/script/manager";
|
||||
import { Channel } from "@App/app/message/channel";
|
||||
import IoC from "@App/app/ioc";
|
||||
import Manager from "@App/app/service/manager";
|
||||
import Hook from "@App/app/service/hook";
|
||||
import { i18nName } from "@App/locales/locales";
|
||||
import { compileInjectScript, compileScriptCode } from "../content/utils";
|
||||
import GMApi, { Request } from "./gm_api";
|
||||
import { genScriptMenu } from "./utils";
|
||||
|
||||
export type RuntimeEvent = "start" | "stop" | "watchRunStatus";
|
||||
|
||||
export type ScriptMenuItem = {
|
||||
id: number;
|
||||
name: string;
|
||||
accessKey?: string;
|
||||
sender: MessageSender;
|
||||
channelFlag: string;
|
||||
};
|
||||
|
||||
export type ScriptMenu = {
|
||||
id: number;
|
||||
name: string;
|
||||
enable: boolean;
|
||||
updatetime: number;
|
||||
hasUserConfig: boolean;
|
||||
metadata: Metadata;
|
||||
runStatus?: SCRIPT_RUN_STATUS;
|
||||
runNum: number;
|
||||
runNumByIframe: number;
|
||||
menus?: ScriptMenuItem[];
|
||||
customExclude?: string[];
|
||||
};
|
||||
|
||||
// 后台脚本将会将代码注入到沙盒中
|
||||
@IoC.Singleton(MessageHander, ResourceManager, ValueManager)
|
||||
export default class Runtime extends Manager {
|
||||
messageSandbox?: MessageSandbox;
|
||||
|
||||
scriptDAO: ScriptDAO;
|
||||
|
||||
resourceManager: ResourceManager;
|
||||
|
||||
valueManager: ValueManager;
|
||||
|
||||
logger: Logger;
|
||||
|
||||
match: UrlMatch<ScriptRunResouce> = new UrlMatch();
|
||||
|
||||
include: UrlInclude<ScriptRunResouce> = new UrlInclude();
|
||||
|
||||
// 自定义排除
|
||||
customizeExclude: UrlMatch<ScriptRunResouce> = new UrlMatch();
|
||||
|
||||
static hook = new Hook<"runStatus">();
|
||||
|
||||
// 运行中和开启的后台脚本
|
||||
runBackScript: Map<number, Script> = new Map();
|
||||
|
||||
constructor(
|
||||
message: MessageHander,
|
||||
resourceManager: ResourceManager,
|
||||
valueManager: ValueManager
|
||||
) {
|
||||
super(message, "runtime");
|
||||
this.scriptDAO = new ScriptDAO();
|
||||
this.resourceManager = resourceManager;
|
||||
this.valueManager = valueManager;
|
||||
this.logger = LoggerCore.getInstance().logger({ component: "runtime" });
|
||||
ScriptManager.hook.addListener("upsert", this.scriptUpdate.bind(this));
|
||||
ScriptManager.hook.addListener("delete", this.scriptDelete.bind(this));
|
||||
ScriptManager.hook.addListener("enable", this.scriptUpdate.bind(this));
|
||||
ScriptManager.hook.addListener("disable", this.scriptUpdate.bind(this));
|
||||
}
|
||||
|
||||
start(): void {
|
||||
// 监听前端消息
|
||||
// 此处是处理执行单次脚本的消息
|
||||
this.listenEvent("start", (id) => {
|
||||
return this.scriptDAO
|
||||
.findById(id)
|
||||
.then((script) => {
|
||||
if (!script) {
|
||||
throw new Error("script not found");
|
||||
}
|
||||
// 因为如果直接引用Runtime,会导致循环依赖,暂时这样处理,后面再梳理梳理
|
||||
return this.startBackgroundScript(script);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger.error("run error", Logger.E(e));
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
|
||||
this.listenEvent("stop", (id) => {
|
||||
return this.scriptDAO
|
||||
.findById(id)
|
||||
.then((script) => {
|
||||
if (!script) {
|
||||
throw new Error("script not found");
|
||||
}
|
||||
// 因为如果直接引用Runtime,会导致循环依赖,暂时这样处理
|
||||
return this.stopBackgroundScript(id);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger.error("stop error", Logger.E(e));
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
// 监听脚本运行状态
|
||||
this.listenScriptRunStatus();
|
||||
|
||||
// 启动普通脚本
|
||||
this.scriptDAO.table.toArray((items) => {
|
||||
items.forEach((item) => {
|
||||
// 容错处理
|
||||
if (!item) {
|
||||
this.logger.error("script is null");
|
||||
return;
|
||||
}
|
||||
if (item.type !== SCRIPT_TYPE_NORMAL) {
|
||||
return;
|
||||
}
|
||||
// 加载所有的脚本
|
||||
if (item.status === SCRIPT_STATUS_ENABLE) {
|
||||
this.enable(item);
|
||||
} else {
|
||||
// 只处理未开启的普通页面脚本
|
||||
this.disable(item);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 接受消息,注入脚本
|
||||
// 获取注入源码
|
||||
|
||||
// 监听菜单创建
|
||||
const scriptMenu: Map<
|
||||
number | TargetTag,
|
||||
Map<
|
||||
number,
|
||||
{
|
||||
request: Request;
|
||||
channel: Channel;
|
||||
}[]
|
||||
>
|
||||
> = new Map();
|
||||
GMApi.hook.addListener(
|
||||
"registerMenu",
|
||||
(request: Request, channel: Channel) => {
|
||||
let senderId: number | TargetTag;
|
||||
if (!request.sender.tabId) {
|
||||
// 非页面脚本
|
||||
senderId = request.sender.targetTag;
|
||||
} else {
|
||||
senderId = request.sender.tabId;
|
||||
}
|
||||
let tabMap = scriptMenu.get(senderId);
|
||||
if (!tabMap) {
|
||||
tabMap = new Map();
|
||||
scriptMenu.set(senderId, tabMap);
|
||||
}
|
||||
let menuArr = tabMap.get(request.uuid);
|
||||
if (!menuArr) {
|
||||
menuArr = [];
|
||||
tabMap.set(request.uuid, menuArr);
|
||||
}
|
||||
// 查询菜单是否已经存在
|
||||
for (let i = 0; i < menuArr.length; i += 1) {
|
||||
// id 相等 跳过,选第一个,并close链接
|
||||
if (menuArr[i].request.params[0] === request.params[0]) {
|
||||
channel.disChannel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
menuArr.push({ request, channel });
|
||||
// 偷懒行为, 直接重新生成菜单
|
||||
genScriptMenu(senderId, scriptMenu);
|
||||
}
|
||||
);
|
||||
GMApi.hook.addListener("unregisterMenu", (id, request: Request) => {
|
||||
let senderId: number | TargetTag;
|
||||
if (!request.sender.tabId) {
|
||||
// 非页面脚本
|
||||
senderId = request.sender.targetTag;
|
||||
} else {
|
||||
senderId = request.sender.tabId;
|
||||
}
|
||||
const tabMap = scriptMenu.get(senderId);
|
||||
if (tabMap) {
|
||||
const menuArr = tabMap.get(request.uuid);
|
||||
if (menuArr) {
|
||||
// 从菜单数组中遍历删除
|
||||
for (let i = 0; i < menuArr.length; i += 1) {
|
||||
if (menuArr[i].request.params[0] === id) {
|
||||
menuArr.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (menuArr.length === 0) {
|
||||
tabMap.delete(request.uuid);
|
||||
}
|
||||
}
|
||||
if (!tabMap.size) {
|
||||
scriptMenu.delete(senderId);
|
||||
}
|
||||
}
|
||||
// 偷懒行为
|
||||
genScriptMenu(senderId, scriptMenu);
|
||||
});
|
||||
|
||||
// 监听页面切换加载菜单
|
||||
chrome.tabs.onActivated.addListener((activeInfo) => {
|
||||
genScriptMenu(activeInfo.tabId, scriptMenu);
|
||||
});
|
||||
|
||||
Runtime.hook.addListener("runStatus", async (scriptId: number) => {
|
||||
const script = await this.scriptDAO.findById(scriptId);
|
||||
if (!script) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
script.status !== SCRIPT_STATUS_ENABLE &&
|
||||
script.runStatus !== "running"
|
||||
) {
|
||||
// 没开启并且不是运行中的脚本,删除
|
||||
this.runBackScript.delete(scriptId);
|
||||
} else {
|
||||
// 否则进行一次更新
|
||||
this.runBackScript.set(scriptId, script);
|
||||
}
|
||||
});
|
||||
|
||||
// 记录运行次数与iframe运行
|
||||
const runScript = new Map<
|
||||
number,
|
||||
Map<number, { script: Script; runNum: number; runNumByIframe: number }>
|
||||
>();
|
||||
const addRunScript = (
|
||||
tabId: number,
|
||||
script: Script,
|
||||
iframe: boolean,
|
||||
num: number = 1
|
||||
) => {
|
||||
let scripts = runScript.get(tabId);
|
||||
if (!scripts) {
|
||||
scripts = new Map();
|
||||
runScript.set(tabId, scripts);
|
||||
}
|
||||
let scriptNum = scripts.get(script.id);
|
||||
if (!scriptNum) {
|
||||
scriptNum = { script, runNum: 0, runNumByIframe: 0 };
|
||||
scripts.set(script.id, scriptNum);
|
||||
}
|
||||
if (script.status === SCRIPT_STATUS_ENABLE) {
|
||||
scriptNum.runNum += num;
|
||||
if (iframe) {
|
||||
scriptNum.runNumByIframe += num;
|
||||
}
|
||||
}
|
||||
};
|
||||
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||
runScript.delete(tabId);
|
||||
});
|
||||
// 给popup页面获取运行脚本,与菜单
|
||||
this.message.setHandler(
|
||||
"queryPageScript",
|
||||
async (action: string, { url, tabId }: any) => {
|
||||
const tabMap = scriptMenu.get(tabId);
|
||||
const run = runScript.get(tabId);
|
||||
let matchScripts = [];
|
||||
if (!run) {
|
||||
matchScripts = this.matchUrl(url).map((item) => {
|
||||
return { runNum: 0, runNumByIframe: 0, script: item };
|
||||
});
|
||||
} else {
|
||||
matchScripts = Array.from(run.values());
|
||||
}
|
||||
const allPromise: Promise<ScriptMenu>[] = matchScripts.map(
|
||||
async (item) => {
|
||||
const menus: ScriptMenuItem[] = [];
|
||||
if (tabMap) {
|
||||
tabMap.get(item.script.id)?.forEach((scriptItem) => {
|
||||
menus.push({
|
||||
name: scriptItem.request.params[1],
|
||||
accessKey: scriptItem.request.params[2],
|
||||
id: scriptItem.request.params[0],
|
||||
sender: scriptItem.request.sender,
|
||||
channelFlag: scriptItem.channel.flag,
|
||||
});
|
||||
});
|
||||
}
|
||||
const script = await this.scriptDAO.findById(item.script.id);
|
||||
if (!script) {
|
||||
return {
|
||||
id: item.script.id,
|
||||
name: i18nName(item.script),
|
||||
enable: item.script.status === SCRIPT_STATUS_ENABLE,
|
||||
updatetime: item.script.updatetime || item.script.createtime,
|
||||
metadata: item.script.metadata,
|
||||
hasUserConfig: !!item.script.config,
|
||||
runNum: item.runNum,
|
||||
runNumByIframe: item.runNumByIframe,
|
||||
customExclude:
|
||||
item.script.selfMetadata && item.script.selfMetadata.exclude,
|
||||
menus,
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: script.id,
|
||||
name: i18nName(script),
|
||||
enable: script.status === SCRIPT_STATUS_ENABLE,
|
||||
updatetime: script.updatetime || script.createtime,
|
||||
metadata: item.script.metadata,
|
||||
hasUserConfig: !!script?.config,
|
||||
runNum: item.runNum,
|
||||
runNumByIframe: item.runNumByIframe,
|
||||
customExclude: script.selfMetadata && script.selfMetadata.exclude,
|
||||
menus,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const scriptList: ScriptMenu[] = await Promise.all(allPromise);
|
||||
|
||||
const backScriptList: ScriptMenu[] = [];
|
||||
const sandboxMenuMap = scriptMenu.get("sandbox");
|
||||
this.runBackScript.forEach((item) => {
|
||||
const menus: ScriptMenuItem[] = [];
|
||||
if (sandboxMenuMap) {
|
||||
sandboxMenuMap?.get(item.id)?.forEach((scriptItem) => {
|
||||
menus.push({
|
||||
name: scriptItem.request.params[1],
|
||||
accessKey: scriptItem.request.params[2],
|
||||
id: scriptItem.request.params[0],
|
||||
sender: scriptItem.request.sender,
|
||||
channelFlag: scriptItem.channel.flag,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
backScriptList.push({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
enable: item.status === SCRIPT_STATUS_ENABLE,
|
||||
updatetime: item.updatetime || item.createtime,
|
||||
metadata: item.metadata,
|
||||
runStatus: item.runStatus,
|
||||
hasUserConfig: !!item.config,
|
||||
runNum:
|
||||
item.runStatus && item.runStatus === SCRIPT_RUN_STATUS_RUNNING
|
||||
? 1
|
||||
: 0,
|
||||
menus,
|
||||
runNumByIframe: 0,
|
||||
});
|
||||
});
|
||||
return Promise.resolve({
|
||||
scriptList,
|
||||
backScriptList,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// content页发送页面加载完成消息,注入脚本
|
||||
this.message.setHandler(
|
||||
"pageLoad",
|
||||
(_action: string, data: any, sender: MessageSender) => {
|
||||
return new Promise((resolve) => {
|
||||
if (!sender) {
|
||||
return;
|
||||
}
|
||||
if (!(sender.url && sender.tabId)) {
|
||||
return;
|
||||
}
|
||||
if (sender.frameId === undefined) {
|
||||
// 清理之前的数据
|
||||
runScript.delete(sender.tabId);
|
||||
}
|
||||
// 未开启
|
||||
if (localStorage.enable_script === "false") {
|
||||
return;
|
||||
}
|
||||
const exclude = this.customizeExclude.match(sender.url);
|
||||
// 自定义排除的, buildScriptRunResource时会将selfMetadata合并,所以后续不需要再处理metadata.exclude,这算是一个隐性的坑,后面看看要不要处理
|
||||
exclude.forEach((val) => {
|
||||
addRunScript(sender.tabId!, val, false, 0);
|
||||
});
|
||||
const filter: ScriptRunResouce[] = this.matchUrl(
|
||||
sender.url,
|
||||
(script) => {
|
||||
// 如果是iframe,判断是否允许在iframe里运行
|
||||
if (sender.frameId !== undefined) {
|
||||
if (script.metadata.noframes) {
|
||||
return true;
|
||||
}
|
||||
addRunScript(sender.tabId!, script, true);
|
||||
return script.status !== SCRIPT_STATUS_ENABLE;
|
||||
}
|
||||
addRunScript(sender.tabId!, script, false);
|
||||
return script.status !== SCRIPT_STATUS_ENABLE;
|
||||
}
|
||||
);
|
||||
|
||||
if (!filter.length) {
|
||||
resolve({ scripts: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({ scripts: filter });
|
||||
|
||||
// 注入脚本
|
||||
filter.forEach((script) => {
|
||||
let runAt = "document_idle";
|
||||
if (script.metadata["run-at"]) {
|
||||
[runAt] = script.metadata["run-at"];
|
||||
}
|
||||
switch (runAt) {
|
||||
case "document-body":
|
||||
case "document-start":
|
||||
runAt = "document_start";
|
||||
break;
|
||||
case "document-end":
|
||||
runAt = "document_end";
|
||||
break;
|
||||
case "document-idle":
|
||||
default:
|
||||
runAt = "document_idle";
|
||||
break;
|
||||
}
|
||||
chrome.tabs.executeScript(sender.tabId!, {
|
||||
frameId: sender.frameId,
|
||||
code: `(function(){
|
||||
let temp = document.createElementNS("http://www.w3.org/1999/xhtml", "script");
|
||||
temp.setAttribute('type', 'text/javascript');
|
||||
temp.innerHTML = "${script.code}";
|
||||
temp.className = "injected-js";
|
||||
document.documentElement.appendChild(temp);
|
||||
temp.remove();
|
||||
}())`,
|
||||
runAt,
|
||||
});
|
||||
});
|
||||
|
||||
// 角标和脚本
|
||||
chrome.browserAction.getBadgeText(
|
||||
{
|
||||
tabId: sender.tabId,
|
||||
},
|
||||
(res: string) => {
|
||||
chrome.browserAction.setBadgeText({
|
||||
text: (filter.length + (parseInt(res, 10) || 0)).toString(),
|
||||
tabId: sender.tabId,
|
||||
});
|
||||
}
|
||||
);
|
||||
chrome.browserAction.setBadgeBackgroundColor({
|
||||
color: "#4e5969",
|
||||
tabId: sender.tabId,
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setMessageSandbox(messageSandbox: MessageSandbox) {
|
||||
this.messageSandbox = messageSandbox;
|
||||
}
|
||||
|
||||
// 启动沙盒相关脚本
|
||||
startSandbox(messageSandbox: MessageSandbox) {
|
||||
this.messageSandbox = messageSandbox;
|
||||
this.scriptDAO.table.toArray((items) => {
|
||||
items.forEach((item) => {
|
||||
// 容错处理
|
||||
if (!item) {
|
||||
this.logger.error("script is null");
|
||||
return;
|
||||
}
|
||||
if (item.type === SCRIPT_TYPE_NORMAL) {
|
||||
return;
|
||||
}
|
||||
// 加载所有的脚本
|
||||
if (item.status === SCRIPT_STATUS_ENABLE) {
|
||||
this.enable(item);
|
||||
this.runBackScript.set(item.id, item);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
listenScriptRunStatus() {
|
||||
// 监听沙盒发送的脚本运行状态消息
|
||||
this.message.setHandler(
|
||||
"scriptRunStatus",
|
||||
(action, [scriptId, runStatus, error, nextruntime]: any) => {
|
||||
this.scriptDAO.update(scriptId, {
|
||||
runStatus,
|
||||
lastruntime: new Date().getTime(),
|
||||
nextruntime,
|
||||
error,
|
||||
});
|
||||
Runtime.hook.trigger("runStatus", scriptId, runStatus);
|
||||
}
|
||||
);
|
||||
// 处理前台发送的脚本运行状态监听请求
|
||||
this.message.setHandlerWithChannel("watchRunStatus", (channel) => {
|
||||
const hook = (scriptId: number, status: SCRIPT_RUN_STATUS) => {
|
||||
channel.send([scriptId, status]);
|
||||
};
|
||||
Runtime.hook.addListener("runStatus", hook);
|
||||
channel.setDisChannelHandler(() => {
|
||||
Runtime.hook.removeListener("runStatus", hook);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 脚本发生变动
|
||||
async scriptUpdate(script: Script): Promise<boolean> {
|
||||
// 脚本更新先更新资源
|
||||
await this.resourceManager.checkScriptResource(script);
|
||||
if (script.status === SCRIPT_STATUS_ENABLE) {
|
||||
return this.enable(script as ScriptRunResouce);
|
||||
}
|
||||
return this.disable(script);
|
||||
}
|
||||
|
||||
matchUrl(url: string, filterFunc?: (script: Script) => boolean) {
|
||||
const scripts = this.match.match(url);
|
||||
// 再include中匹配
|
||||
scripts.push(...this.include.match(url));
|
||||
const filter: { [key: string]: ScriptRunResouce } = {};
|
||||
// 去重
|
||||
scripts.forEach((script) => {
|
||||
if (filterFunc && filterFunc(script)) {
|
||||
return;
|
||||
}
|
||||
filter[script.id] = script;
|
||||
});
|
||||
// 转换成数组
|
||||
return Object.keys(filter).map((key) => filter[key]);
|
||||
}
|
||||
|
||||
// 脚本删除
|
||||
async scriptDelete(script: Script): Promise<boolean> {
|
||||
// 清理匹配资源
|
||||
if (script.type === SCRIPT_TYPE_NORMAL) {
|
||||
this.match.del(<ScriptRunResouce>script);
|
||||
this.include.del(<ScriptRunResouce>script);
|
||||
} else {
|
||||
this.unloadBackgroundScript(script);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// 脚本开启
|
||||
async enable(script: Script): Promise<boolean> {
|
||||
// 编译脚本运行资源
|
||||
const scriptRes = await this.buildScriptRunResource(script);
|
||||
if (script.type !== SCRIPT_TYPE_NORMAL) {
|
||||
return this.loadBackgroundScript(scriptRes);
|
||||
}
|
||||
return this.loadPageScript(scriptRes);
|
||||
}
|
||||
|
||||
// 脚本关闭
|
||||
disable(script: Script): Promise<boolean> {
|
||||
if (script.type !== SCRIPT_TYPE_NORMAL) {
|
||||
return this.unloadBackgroundScript(script);
|
||||
}
|
||||
return this.unloadPageScript(script);
|
||||
}
|
||||
|
||||
// 加载页面脚本
|
||||
loadPageScript(script: ScriptRunResouce) {
|
||||
// 重构code
|
||||
const logger = this.logger.with({
|
||||
scriptId: script.id,
|
||||
name: script.name,
|
||||
});
|
||||
script.code = dealScript(compileInjectScript(script));
|
||||
|
||||
this.match.del(<ScriptRunResouce>script);
|
||||
this.include.del(<ScriptRunResouce>script);
|
||||
if (script.metadata.match) {
|
||||
script.metadata.match.forEach((url) => {
|
||||
try {
|
||||
this.match.add(url, script);
|
||||
} catch (e) {
|
||||
logger.error("url load error", Logger.E(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (script.metadata.include) {
|
||||
script.metadata.include.forEach((url) => {
|
||||
try {
|
||||
this.include.add(url, script);
|
||||
} catch (e) {
|
||||
logger.error("url load error", Logger.E(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (script.metadata.exclude) {
|
||||
script.metadata.exclude.forEach((url) => {
|
||||
try {
|
||||
this.include.exclude(url, script);
|
||||
this.match.exclude(url, script);
|
||||
} catch (e) {
|
||||
logger.error("url load error", Logger.E(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (script.selfMetadata && script.selfMetadata.exclude) {
|
||||
script.selfMetadata.exclude.forEach((url) => {
|
||||
try {
|
||||
this.customizeExclude.add(url, script);
|
||||
} catch (e) {
|
||||
logger.error("url load error", Logger.E(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// 卸载页面脚本
|
||||
unloadPageScript(script: Script) {
|
||||
return this.loadPageScript(<ScriptRunResouce>script);
|
||||
}
|
||||
|
||||
// 加载并启动后台脚本
|
||||
loadBackgroundScript(script: ScriptRunResouce): Promise<boolean> {
|
||||
this.runBackScript.set(script.id, script);
|
||||
return new Promise((resolve, reject) => {
|
||||
// 清除重试数据
|
||||
script.nextruntime = 0;
|
||||
this.messageSandbox
|
||||
?.syncSend("enable", script)
|
||||
.then(() => {
|
||||
resolve(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error("backscript load error", Logger.E(err));
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 卸载并停止后台脚本
|
||||
unloadBackgroundScript(script: Script): Promise<boolean> {
|
||||
this.runBackScript.delete(script.id);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.messageSandbox
|
||||
?.syncSend("disable", script.id)
|
||||
.then(() => {
|
||||
resolve(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error("backscript stop error", Logger.E(err));
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async startBackgroundScript(script: Script) {
|
||||
const scriptRes = await this.buildScriptRunResource(script);
|
||||
this.messageSandbox?.syncSend("start", scriptRes);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
stopBackgroundScript(scriptId: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.messageSandbox
|
||||
?.syncSend("stop", scriptId)
|
||||
.then((resp) => {
|
||||
resolve(resp);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error("backscript stop error", Logger.E(err));
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async buildScriptRunResource(script: Script): Promise<ScriptRunResouce> {
|
||||
const ret: ScriptRunResouce = <ScriptRunResouce>Object.assign(script);
|
||||
|
||||
// 自定义配置
|
||||
if (ret.selfMetadata) {
|
||||
ret.metadata = { ...ret.metadata };
|
||||
Object.keys(ret.selfMetadata).forEach((key) => {
|
||||
ret.metadata[key] = ret.selfMetadata![key];
|
||||
});
|
||||
}
|
||||
|
||||
ret.value = await this.valueManager.getScriptValues(ret);
|
||||
|
||||
ret.resource = await this.resourceManager.getScriptResources(ret);
|
||||
|
||||
ret.flag = randomString(16);
|
||||
ret.sourceCode = ret.code;
|
||||
ret.code = compileScriptCode(ret);
|
||||
|
||||
ret.grantMap = {};
|
||||
|
||||
ret.metadata.grant?.forEach((val: string) => {
|
||||
ret.grantMap[val] = "ok";
|
||||
});
|
||||
|
||||
return Promise.resolve(ret);
|
||||
}
|
||||
}
|
@@ -1,535 +0,0 @@
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import { Channel } from "@App/app/message/channel";
|
||||
import { SCRIPT_STATUS_ENABLE, Script } from "@App/app/repo/scripts";
|
||||
import { isFirefox } from "@App/pkg/utils/utils";
|
||||
import MessageCenter from "@App/app/message/center";
|
||||
import IoC from "@App/app/ioc";
|
||||
import { Request } from "./gm_api";
|
||||
import Runtime from "./runtime";
|
||||
|
||||
export const unsafeHeaders: { [key: string]: boolean } = {
|
||||
// 部分浏览器中并未允许
|
||||
"user-agent": true,
|
||||
// 这两个是前缀
|
||||
"proxy-": true,
|
||||
"sec-": true,
|
||||
// cookie已经特殊处理
|
||||
cookie: true,
|
||||
"accept-charset": true,
|
||||
"accept-encoding": true,
|
||||
"access-control-request-headers": true,
|
||||
"access-control-request-method": true,
|
||||
connection: true,
|
||||
"content-length": true,
|
||||
date: true,
|
||||
dnt: true,
|
||||
expect: true,
|
||||
"feature-policy": true,
|
||||
host: true,
|
||||
"keep-alive": true,
|
||||
origin: true,
|
||||
referer: true,
|
||||
te: true,
|
||||
trailer: true,
|
||||
"transfer-encoding": true,
|
||||
upgrade: true,
|
||||
via: true,
|
||||
};
|
||||
|
||||
export const responseHeaders: { [key: string]: boolean } = {
|
||||
"set-cookie": true,
|
||||
};
|
||||
|
||||
export function isUnsafeHeaders(header: string) {
|
||||
return unsafeHeaders[header.toLocaleLowerCase()];
|
||||
}
|
||||
|
||||
export function isExtensionRequest(
|
||||
details: chrome.webRequest.ResourceRequest & { originUrl?: string }
|
||||
): boolean {
|
||||
return !!(
|
||||
(details.initiator &&
|
||||
chrome.runtime.getURL("").startsWith(details.initiator)) ||
|
||||
(details.originUrl &&
|
||||
details.originUrl.startsWith(chrome.runtime.getURL("")))
|
||||
);
|
||||
}
|
||||
|
||||
// 监听web请求,处理unsafeHeaders
|
||||
export function listenerWebRequest(headerFlag: string) {
|
||||
const reqOpt = ["blocking", "requestHeaders"];
|
||||
const respOpt = ["blocking", "responseHeaders"];
|
||||
if (!isFirefox()) {
|
||||
reqOpt.push("extraHeaders");
|
||||
respOpt.push("extraHeaders");
|
||||
}
|
||||
const maxRedirects = new Map<string, [number, number]>();
|
||||
const isRedirects = new Map<string, boolean>();
|
||||
// 处理发送请求的unsafeHeaders
|
||||
chrome.webRequest.onBeforeSendHeaders.addListener(
|
||||
(details) => {
|
||||
if (!isExtensionRequest(details)) {
|
||||
return {};
|
||||
}
|
||||
// 处理unsafeHeaders
|
||||
let cookie = "";
|
||||
let setCookie = "";
|
||||
let anonymous = false;
|
||||
let isGmXhr = false;
|
||||
const requestHeaders: chrome.webRequest.HttpHeader[] = [];
|
||||
const preRequestHeaders: { [key: string]: string | null } = {};
|
||||
details.requestHeaders?.forEach((val) => {
|
||||
const lowerCase = val.name.toLowerCase();
|
||||
if (lowerCase.startsWith(`${headerFlag}-`)) {
|
||||
const headerKey = lowerCase.substring(headerFlag.length + 1);
|
||||
// 处理unsafeHeaders
|
||||
switch (headerKey) {
|
||||
case "cookie":
|
||||
setCookie = val.value || "";
|
||||
break;
|
||||
case "max-redirects":
|
||||
maxRedirects.set(details.requestId, [
|
||||
0,
|
||||
parseInt(val.value || "", 10),
|
||||
]);
|
||||
break;
|
||||
case "anonymous":
|
||||
anonymous = true;
|
||||
break;
|
||||
case "gm-xhr":
|
||||
isGmXhr = true;
|
||||
break;
|
||||
default:
|
||||
preRequestHeaders[headerKey] = val.value || null;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 原生header
|
||||
switch (lowerCase) {
|
||||
case "cookie":
|
||||
cookie = val.value || "";
|
||||
break;
|
||||
default:
|
||||
// 如果是unsafeHeaders,则判断是否已经有值,有值则不进行处理
|
||||
if (
|
||||
unsafeHeaders[lowerCase] ||
|
||||
lowerCase.startsWith("sec-") ||
|
||||
lowerCase.startsWith("proxy-")
|
||||
) {
|
||||
// null表示不发送此header
|
||||
if (preRequestHeaders[lowerCase] !== null) {
|
||||
preRequestHeaders[lowerCase] =
|
||||
preRequestHeaders[lowerCase] || val.value || "";
|
||||
}
|
||||
} else {
|
||||
requestHeaders.push(val);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
// 不是由GM XHR发起的请求,不处理
|
||||
if (!isGmXhr) {
|
||||
return {};
|
||||
}
|
||||
// 匿名移除掉cookie
|
||||
if (anonymous) {
|
||||
cookie = "";
|
||||
}
|
||||
// 有设置cookie,则进行处理
|
||||
if (setCookie) {
|
||||
// 判断结尾是否有分号,没有则添加,然后进行拼接
|
||||
if (!cookie || cookie.endsWith(";")) {
|
||||
cookie += setCookie;
|
||||
} else {
|
||||
cookie += `;${setCookie}`;
|
||||
}
|
||||
}
|
||||
// 有cookie,则进行处理
|
||||
if (cookie) {
|
||||
requestHeaders.push({
|
||||
name: "Cookie",
|
||||
value: cookie,
|
||||
});
|
||||
}
|
||||
Object.keys(preRequestHeaders).forEach((key) => {
|
||||
// null表示不发送此header
|
||||
if (preRequestHeaders[key] !== null) {
|
||||
requestHeaders.push({
|
||||
name: key,
|
||||
value: preRequestHeaders[key]!,
|
||||
});
|
||||
}
|
||||
});
|
||||
return {
|
||||
requestHeaders,
|
||||
};
|
||||
},
|
||||
{
|
||||
urls: ["<all_urls>"],
|
||||
},
|
||||
reqOpt
|
||||
);
|
||||
// 处理无法读取的responseHeaders
|
||||
chrome.webRequest.onHeadersReceived.addListener(
|
||||
(details) => {
|
||||
if (!isExtensionRequest(details)) {
|
||||
// 判断是否为页面请求
|
||||
if (
|
||||
!(details.type === "main_frame" || details.type === "sub_frame") ||
|
||||
!isFirefox()
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
// 判断页面上是否有脚本会运行,如果有判断是否有csp,有则移除csp策略
|
||||
const runtime = IoC.instance(Runtime) as Runtime;
|
||||
// 这块代码与runtime里的pageLoad一样,考虑后面要不要优化
|
||||
const result = runtime.matchUrl(details.url, (script) => {
|
||||
// 如果是iframe,判断是否允许在iframe里运行
|
||||
if (details.type === "sub_frame") {
|
||||
if (script.metadata.noframes) {
|
||||
return true;
|
||||
}
|
||||
return script.status !== SCRIPT_STATUS_ENABLE;
|
||||
}
|
||||
return script.status !== SCRIPT_STATUS_ENABLE;
|
||||
});
|
||||
if (result.length > 0 && details.responseHeaders) {
|
||||
// 移除csp
|
||||
for (let i = 0; i < details.responseHeaders.length; i += 1) {
|
||||
if (
|
||||
details.responseHeaders[i].name.toLowerCase() ===
|
||||
"content-security-policy"
|
||||
) {
|
||||
details.responseHeaders[i].value = "";
|
||||
}
|
||||
}
|
||||
return {
|
||||
responseHeaders: details.responseHeaders,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
const appendHeaders: chrome.webRequest.HttpHeader[] = [];
|
||||
details.responseHeaders?.forEach((val) => {
|
||||
const lowerCase = val.name.toLowerCase();
|
||||
if (responseHeaders[lowerCase]) {
|
||||
const copy = { ...val };
|
||||
copy.name = `${headerFlag}-${val.name}`;
|
||||
appendHeaders.push(copy);
|
||||
}
|
||||
// 处理最大重定向次数
|
||||
if (lowerCase === "location") {
|
||||
isRedirects.set(details.requestId, true);
|
||||
const nums = maxRedirects.get(details.requestId);
|
||||
if (nums) {
|
||||
nums[0] += 1;
|
||||
// 当前重定向次数大于最大重定向次数时,修改掉locatin,防止重定向
|
||||
if (nums[0] > nums[1]) {
|
||||
val.name = `${headerFlag}-${val.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
details.responseHeaders?.push(...appendHeaders);
|
||||
// 判断是否为重定向请求,如果是,将url注入到finalUrl
|
||||
if (isRedirects.has(details.requestId)) {
|
||||
details.responseHeaders?.push({
|
||||
name: `${headerFlag}-final-url`,
|
||||
value: details.url,
|
||||
});
|
||||
}
|
||||
return {
|
||||
responseHeaders: details.responseHeaders,
|
||||
};
|
||||
},
|
||||
{
|
||||
urls: ["<all_urls>"],
|
||||
},
|
||||
respOpt
|
||||
);
|
||||
chrome.webRequest.onCompleted.addListener(
|
||||
(details) => {
|
||||
if (!isExtensionRequest(details)) {
|
||||
return;
|
||||
}
|
||||
// 删除最大重定向数缓存
|
||||
maxRedirects.delete(details.requestId);
|
||||
isRedirects.delete(details.requestId);
|
||||
},
|
||||
{ urls: ["<all_urls>"] }
|
||||
);
|
||||
}
|
||||
|
||||
// 给xhr添加headers,包括unsafeHeaders
|
||||
export function setXhrHeader(
|
||||
headerFlag: string,
|
||||
config: GMSend.XHRDetails,
|
||||
xhr: XMLHttpRequest
|
||||
) {
|
||||
xhr.setRequestHeader(`${headerFlag}-gm-xhr`, "true");
|
||||
if (config.headers) {
|
||||
let hasOrigin = false;
|
||||
Object.keys(config.headers).forEach((key) => {
|
||||
const lowKey = key.toLowerCase();
|
||||
if (lowKey === "origin") {
|
||||
hasOrigin = true;
|
||||
}
|
||||
try {
|
||||
if (
|
||||
unsafeHeaders[lowKey] ||
|
||||
lowKey.startsWith("sec-") ||
|
||||
lowKey.startsWith("proxy-")
|
||||
) {
|
||||
xhr.setRequestHeader(
|
||||
`${headerFlag}-${lowKey}`,
|
||||
config.headers![key]!
|
||||
);
|
||||
} else {
|
||||
// 直接设置header
|
||||
xhr.setRequestHeader(key, config.headers![key]!);
|
||||
}
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error(
|
||||
"GM XHR setRequestHeader error"
|
||||
);
|
||||
}
|
||||
});
|
||||
if (!hasOrigin) {
|
||||
xhr.setRequestHeader(`${headerFlag}-origin`, "");
|
||||
}
|
||||
}
|
||||
if (config.maxRedirects !== undefined) {
|
||||
xhr.setRequestHeader(
|
||||
`${headerFlag}-max-redirects`,
|
||||
config.maxRedirects.toString()
|
||||
);
|
||||
}
|
||||
if (config.cookie) {
|
||||
try {
|
||||
xhr.setRequestHeader(`${headerFlag}-cookie`, config.cookie);
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error(
|
||||
"GM XHR setRequestHeader cookie error"
|
||||
);
|
||||
}
|
||||
}
|
||||
if (config.anonymous) {
|
||||
xhr.setRequestHeader(`${headerFlag}-anonymous`, "true");
|
||||
}
|
||||
}
|
||||
|
||||
export function getFetchHeader(
|
||||
headerFlag: string,
|
||||
config: GMSend.XHRDetails
|
||||
): any {
|
||||
const headers: { [key: string]: string } = {};
|
||||
headers[`${headerFlag}-gm-xhr`] = "true";
|
||||
if (config.headers) {
|
||||
Object.keys(config.headers).forEach((key) => {
|
||||
const lowKey = key.toLowerCase();
|
||||
if (
|
||||
unsafeHeaders[lowKey] ||
|
||||
lowKey.startsWith("sec-") ||
|
||||
lowKey.startsWith("proxy-")
|
||||
) {
|
||||
headers[`${headerFlag}-${lowKey}`] = config.headers![key]!;
|
||||
} else {
|
||||
// 直接设置header
|
||||
headers[key] = config.headers![key]!;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (config.maxRedirects !== undefined) {
|
||||
headers[`${headerFlag}-max-redirects`] = config.maxRedirects.toString();
|
||||
}
|
||||
if (config.cookie) {
|
||||
headers[`${headerFlag}-cookie`] = config.cookie;
|
||||
}
|
||||
if (config.anonymous) {
|
||||
headers[`${headerFlag}-anonymous`] = "true";
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export async function dealXhr(
|
||||
headerFlag: string,
|
||||
config: GMSend.XHRDetails,
|
||||
xhr: XMLHttpRequest
|
||||
): Promise<GMTypes.XHRResponse> {
|
||||
let finalUrl = xhr.responseURL || config.url;
|
||||
// 判断是否有headerFlag-final-url,有则替换finalUrl
|
||||
const finalUrlHeader = xhr.getResponseHeader(`${headerFlag}-final-url`);
|
||||
if (finalUrlHeader) {
|
||||
finalUrl = finalUrlHeader;
|
||||
}
|
||||
const removeXCat = new RegExp(`${headerFlag}-`, "g");
|
||||
const respond: GMTypes.XHRResponse = {
|
||||
finalUrl,
|
||||
readyState: <any>xhr.readyState,
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText,
|
||||
responseHeaders: xhr.getAllResponseHeaders().replace(removeXCat, ""),
|
||||
responseType: config.responseType,
|
||||
};
|
||||
if (xhr.readyState === 4) {
|
||||
if (
|
||||
config.responseType?.toLowerCase() === "arraybuffer" ||
|
||||
config.responseType?.toLowerCase() === "blob"
|
||||
) {
|
||||
let blob: Blob;
|
||||
if (xhr.response instanceof ArrayBuffer) {
|
||||
blob = new Blob([xhr.response]);
|
||||
respond.response = URL.createObjectURL(blob);
|
||||
} else {
|
||||
blob = <Blob>xhr.response;
|
||||
respond.response = URL.createObjectURL(blob);
|
||||
}
|
||||
try {
|
||||
if (xhr.getResponseHeader("Content-Type")?.indexOf("text") !== -1) {
|
||||
// 如果是文本类型,则尝试转换为文本
|
||||
respond.responseText = await blob.text();
|
||||
}
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error(
|
||||
"GM XHR getResponseHeader error"
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(<string>respond.response);
|
||||
}, 60e3);
|
||||
} else if (config.responseType === "json") {
|
||||
try {
|
||||
respond.response = JSON.parse(xhr.responseText);
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error("GM XHR JSON parse error");
|
||||
}
|
||||
try {
|
||||
respond.responseText = xhr.responseText;
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error("GM XHR getResponseText error");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
respond.response = xhr.response;
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error("GM XHR response error");
|
||||
}
|
||||
try {
|
||||
respond.responseText = xhr.responseText || undefined;
|
||||
} catch (e) {
|
||||
LoggerCore.getLogger(Logger.E(e)).error("GM XHR getResponseText error");
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(respond);
|
||||
}
|
||||
|
||||
export function dealFetch(
|
||||
headerFlag: string,
|
||||
config: GMSend.XHRDetails,
|
||||
response: Response,
|
||||
readyState: 0 | 1 | 2 | 3 | 4
|
||||
) {
|
||||
const removeXCat = new RegExp(`${headerFlag}-`, "g");
|
||||
let respHeader = "";
|
||||
response.headers &&
|
||||
response.headers.forEach((value, key) => {
|
||||
respHeader += `${key.replace(removeXCat, "")}: ${value}\n`;
|
||||
});
|
||||
const respond: GMTypes.XHRResponse = {
|
||||
finalUrl: response.url || config.url,
|
||||
readyState,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
responseHeaders: respHeader,
|
||||
responseType: config.responseType,
|
||||
};
|
||||
return respond;
|
||||
}
|
||||
|
||||
export function getIcon(script: Script): string {
|
||||
return (
|
||||
(script.metadata.icon && script.metadata.icon[0]) ||
|
||||
(script.metadata.iconurl && script.metadata.iconurl[0]) ||
|
||||
(script.metadata.defaulticon && script.metadata.defaulticon[0]) ||
|
||||
(script.metadata.icon64 && script.metadata.icon64[0]) ||
|
||||
(script.metadata.icon64url && script.metadata.icon64url[0])
|
||||
);
|
||||
}
|
||||
function genScriptMenuByTabMap(
|
||||
tabMap: Map<number, { request: Request; channel: Channel }[]>
|
||||
) {
|
||||
tabMap.forEach((menuArr, scriptId) => {
|
||||
// 创建脚本菜单
|
||||
chrome.contextMenus.create({
|
||||
id: `scriptMenu_${scriptId}`,
|
||||
title: menuArr[0].request.script.name,
|
||||
contexts: ["all"],
|
||||
parentId: "scriptMenu",
|
||||
});
|
||||
menuArr.forEach((menu) => {
|
||||
// 创建菜单
|
||||
chrome.contextMenus.create({
|
||||
id: `scriptMenu_menu_${scriptId}_${menu.request.params[0]}`,
|
||||
title: menu.request.params[1],
|
||||
contexts: ["all"],
|
||||
parentId: `scriptMenu_${scriptId}`,
|
||||
onclick: () => {
|
||||
(IoC.instance(MessageCenter) as MessageCenter).sendNative(
|
||||
{
|
||||
tag: menu.request.sender.targetTag,
|
||||
id: [
|
||||
menu.request.sender.frameId || menu.request.sender.tabId || 0,
|
||||
],
|
||||
},
|
||||
{
|
||||
stream: menu.channel.flag,
|
||||
channel: true,
|
||||
data: "click",
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 生成chrome菜单
|
||||
export function genScriptMenu(
|
||||
tabId: number | string,
|
||||
scriptMenu: Map<
|
||||
number | string,
|
||||
Map<
|
||||
number,
|
||||
{
|
||||
request: Request;
|
||||
channel: Channel;
|
||||
}[]
|
||||
>
|
||||
>
|
||||
) {
|
||||
// 移除之前所有的菜单
|
||||
chrome.contextMenus.removeAll();
|
||||
const tabMap = scriptMenu.get(tabId);
|
||||
const backTabMap = scriptMenu.get("sandbox");
|
||||
if (!tabMap && !backTabMap) {
|
||||
return;
|
||||
}
|
||||
// 创建根菜单
|
||||
chrome.contextMenus.create({
|
||||
id: "scriptMenu",
|
||||
title: "ScriptCat",
|
||||
contexts: ["all"],
|
||||
});
|
||||
if (tabMap) {
|
||||
genScriptMenuByTabMap(tabMap);
|
||||
}
|
||||
// 后台脚本的菜单
|
||||
if (tabId !== "sandbox") {
|
||||
if (backTabMap) {
|
||||
genScriptMenuByTabMap(backTabMap);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,37 +0,0 @@
|
||||
import { Script } from "@App/app/repo/scripts";
|
||||
|
||||
export const unsafeHeaders: { [key: string]: boolean } = {
|
||||
// 部分浏览器中并未允许
|
||||
"user-agent": true,
|
||||
// 这两个是前缀
|
||||
"proxy-": true,
|
||||
"sec-": true,
|
||||
// cookie已经特殊处理
|
||||
cookie: true,
|
||||
"accept-charset": true,
|
||||
"accept-encoding": true,
|
||||
"access-control-request-headers": true,
|
||||
"access-control-request-method": true,
|
||||
connection: true,
|
||||
"content-length": true,
|
||||
date: true,
|
||||
dnt: true,
|
||||
expect: true,
|
||||
"feature-policy": true,
|
||||
host: true,
|
||||
"keep-alive": true,
|
||||
origin: true,
|
||||
referer: true,
|
||||
te: true,
|
||||
trailer: true,
|
||||
"transfer-encoding": true,
|
||||
upgrade: true,
|
||||
via: true,
|
||||
};
|
||||
|
||||
export function getStorageName(script: Script): string {
|
||||
if (script.metadata && script.metadata.storagename) {
|
||||
return script.metadata.storagename[0];
|
||||
}
|
||||
return script.uuid;
|
||||
}
|
@@ -8,6 +8,9 @@ import { Server } from "@Packages/message/server";
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { ServiceWorkerMessageSend } from "@Packages/message/window_message";
|
||||
|
||||
// 初始化数据库
|
||||
migrate();
|
||||
|
||||
const OFFSCREEN_DOCUMENT_PATH = "src/offscreen.html";
|
||||
|
||||
let creating: Promise<void> | null;
|
||||
@@ -46,8 +49,6 @@ async function setupOffscreenDocument() {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 初始化数据库
|
||||
migrate();
|
||||
// 初始化日志组件
|
||||
const loggerCore = new LoggerCore({
|
||||
writer: new DBWriter(new LoggerDAO()),
|
||||
@@ -55,9 +56,10 @@ async function main() {
|
||||
});
|
||||
loggerCore.logger().debug("service worker start");
|
||||
// 初始化管理器
|
||||
const message = new ExtensionMessage();
|
||||
const message = new ExtensionMessage(true);
|
||||
const server = new Server("serviceWorker", message);
|
||||
const manager = new ServiceWorkerManager(server, new MessageQueue(), new ServiceWorkerMessageSend());
|
||||
const messageQueue = new MessageQueue();
|
||||
const manager = new ServiceWorkerManager(server, messageQueue, new ServiceWorkerMessageSend());
|
||||
manager.initManager();
|
||||
// 初始化沙盒环境
|
||||
await setupOffscreenDocument();
|
||||
|
457
src/template/scriptcat.d.tpl
Normal file
457
src/template/scriptcat.d.tpl
Normal file
@@ -0,0 +1,457 @@
|
||||
// @copyright https://github.com/silverwzw/Tampermonkey-Typescript-Declaration
|
||||
|
||||
declare const unsafeWindow: Window;
|
||||
|
||||
declare type ConfigType = "text" | "checkbox" | "select" | "mult-select" | "number" | "textarea" | "time";
|
||||
|
||||
declare interface Config {
|
||||
[key: string]: unknown;
|
||||
title: string;
|
||||
description: string;
|
||||
default?: unknown;
|
||||
type?: ConfigType;
|
||||
bind?: string;
|
||||
values?: unknown[];
|
||||
password?: boolean;
|
||||
// 文本类型时是字符串长度,数字类型时是最大值
|
||||
max?: number;
|
||||
min?: number;
|
||||
rows?: number; // textarea行数
|
||||
}
|
||||
|
||||
declare type UserConfig = { [key: string]: { [key: string]: Config } };
|
||||
|
||||
declare const GM_info: {
|
||||
version: string;
|
||||
scriptWillUpdate: boolean;
|
||||
scriptHandler: "ScriptCat";
|
||||
scriptUpdateURL?: string;
|
||||
// scriptSource: string;
|
||||
scriptMetaStr?: string;
|
||||
userConfig?: UserConfig;
|
||||
userConfigStr?: string;
|
||||
// isIncognito: boolean;
|
||||
// downloadMode: "native" | "disabled" | "browser";
|
||||
script: {
|
||||
author?: string;
|
||||
description?: string;
|
||||
// excludes: string[];
|
||||
grant: string[];
|
||||
header: string;
|
||||
// homepage?: string;
|
||||
icon?: string;
|
||||
icon64?: string;
|
||||
includes?: string[];
|
||||
// lastModified: number;
|
||||
matches: string[];
|
||||
name: string;
|
||||
namespace?: string;
|
||||
// position: number;
|
||||
"run-at": string;
|
||||
// resources: string[];
|
||||
// unwrap: boolean;
|
||||
version: string;
|
||||
/* options: {
|
||||
awareOfChrome: boolean;
|
||||
run_at: string;
|
||||
noframes?: boolean;
|
||||
compat_arrayLeft: boolean;
|
||||
compat_foreach: boolean;
|
||||
compat_forvarin: boolean;
|
||||
compat_metadata: boolean;
|
||||
compat_uW_gmonkey: boolean;
|
||||
override: {
|
||||
orig_excludes: string[];
|
||||
orig_includes: string[];
|
||||
use_includes: string[];
|
||||
use_excludes: string[];
|
||||
[key: string]: any;
|
||||
};
|
||||
[key: string]: any;
|
||||
}; */
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
declare function GM_addStyle(css: string): HTMLElement;
|
||||
|
||||
declare function GM_deleteValue(name: string): void;
|
||||
|
||||
declare function GM_listValues(): string[];
|
||||
|
||||
declare function GM_addValueChangeListener(name: string, listener: GMTypes.ValueChangeListener): number;
|
||||
|
||||
declare function GM_removeValueChangeListener(listenerId: number): void;
|
||||
|
||||
// 可以使用Promise实际等待值的设置完成
|
||||
declare function GM_setValue(name: string, value: unknown): Promise;
|
||||
|
||||
declare function GM_getValue(name: string, defaultValue?: unknown): unknown;
|
||||
|
||||
// 支持level和label
|
||||
declare function GM_log(message: string, level?: GMTypes.LoggerLevel, labels?: GMTypes.LoggerLabel): unknown;
|
||||
|
||||
declare function GM_getResourceText(name: string): string | undefined;
|
||||
|
||||
declare function GM_getResourceURL(name: string, isBlobUrl?: boolean): string | undefined;
|
||||
|
||||
declare function GM_registerMenuCommand(name: string, listener: () => void, accessKey?: string): number;
|
||||
|
||||
declare function GM_unregisterMenuCommand(id: number): void;
|
||||
|
||||
declare function GM_openInTab(url: string, options: GMTypes.OpenTabOptions): tab;
|
||||
declare function GM_openInTab(url: string, loadInBackground: boolean): tab;
|
||||
declare function GM_openInTab(url: string): tab;
|
||||
|
||||
declare function GM_xmlhttpRequest(details: GMTypes.XHRDetails): GMTypes.AbortHandle<void>;
|
||||
|
||||
declare function GM_download(details: GMTypes.DownloadDetails): GMTypes.AbortHandle<boolean>;
|
||||
declare function GM_download(url: string, filename: string): GMTypes.AbortHandle<boolean>;
|
||||
|
||||
declare function GM_getTab(callback: (obj: object) => unknown): void;
|
||||
|
||||
declare function GM_saveTab(obj: object): Promise<void>;
|
||||
|
||||
declare function GM_getTabs(callback: (objs: { [key: number]: object }) => unknown): void;
|
||||
|
||||
declare function GM_notification(details: GMTypes.NotificationDetails, ondone?: GMTypes.NotificationOnDone): void;
|
||||
declare function GM_notification(
|
||||
text: string,
|
||||
title: string,
|
||||
image: string,
|
||||
onclick?: GMTypes.NotificationOnClick
|
||||
): void;
|
||||
|
||||
declare function GM_closeNotification(id: string): void;
|
||||
|
||||
declare function GM_updateNotification(id: string, details: GMTypes.NotificationDetails): void;
|
||||
|
||||
declare function GM_setClipboard(data: string, info?: string | { type?: string; minetype?: string }): void;
|
||||
|
||||
declare function GM_addElement(tag: string, attribubutes: unknown);
|
||||
declare function GM_addElement(parentNode: Element, tag: string, attrs: unknown);
|
||||
|
||||
// name和domain不能都为空
|
||||
declare function GM_cookie(
|
||||
action: GMTypes.CookieAction,
|
||||
details: GMTypes.CookieDetails,
|
||||
ondone: (cookie: GMTypes.Cookie[], error: unknown | undefined) => void
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 可以通过GM_addValueChangeListener获取tabid
|
||||
* 再通过tabid(前后端通信可能用到,ValueChangeListener会返回tabid),获取storeid,后台脚本用.
|
||||
* 请注意这是一个实验性质的API,后续可能会改变
|
||||
* @param tabid 页面的tabid
|
||||
* @param ondone 完成事件
|
||||
* @param callback.storeid 该页面的storeid,可以给GM_cookie使用
|
||||
* @param callback.error 错误信息
|
||||
* @deprecated 已废弃,请使用GM_cookie("store", tabid)替代
|
||||
*/
|
||||
declare function GM_getCookieStore(
|
||||
tabid: number,
|
||||
ondone: (storeId: number | undefined, error: unknown | undefined) => void
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 设置浏览器代理
|
||||
* @deprecated 正式版中已废弃,后续可能会在beta版本中添加
|
||||
*/
|
||||
declare function CAT_setProxy(rule: CATType.ProxyRule[] | string): void;
|
||||
|
||||
/**
|
||||
* 清理所有代理规则
|
||||
* @deprecated 正式版中已废弃,后续可能会在beta版本中添加
|
||||
*/
|
||||
declare function CAT_clearProxy(): void;
|
||||
|
||||
/**
|
||||
* 输入x、y,模拟真实点击
|
||||
* @deprecated 正式版中已废弃,后续可能会在beta版本中添加
|
||||
*/
|
||||
declare function CAT_click(x: number, y: number): void;
|
||||
|
||||
/**
|
||||
* 打开脚本的用户配置页面
|
||||
*/
|
||||
declare function CAT_userConfig(): void;
|
||||
|
||||
/**
|
||||
* 操控管理器设置的储存系统,将会在目录下创建一个app/uuid目录供此 API 使用,如果指定了baseDir参数,则会使用baseDir作为基础目录
|
||||
* 上传时默认覆盖同名文件
|
||||
* @param action 操作类型 list 列出指定目录所有文件, upload 上传文件, download 下载文件, delete 删除文件, config 打开配置页, 暂时不提供move/mkdir等操作
|
||||
* @param details
|
||||
*/
|
||||
declare function CAT_fileStorage(
|
||||
action: "list",
|
||||
details: {
|
||||
// 文件路径
|
||||
path?: string;
|
||||
// 基础目录,如果未设置,则将脚本uuid作为目录
|
||||
baseDir?: string;
|
||||
onload?: (files: CATType.FileStorageFileInfo[]) => void;
|
||||
onerror?: (error: CATType.FileStorageError) => void;
|
||||
}
|
||||
): void;
|
||||
declare function CAT_fileStorage(
|
||||
action: "download",
|
||||
details: {
|
||||
file: CATType.FileStorageFileInfo; // 某些平台需要提供文件的hash值,所以需要传入文件信息
|
||||
onload: (data: Blob) => void;
|
||||
// onprogress?: (progress: number) => void;
|
||||
onerror?: (error: CATType.FileStorageError) => void;
|
||||
// public?: boolean;
|
||||
}
|
||||
): void;
|
||||
declare function CAT_fileStorage(
|
||||
action: "delete",
|
||||
details: {
|
||||
path: string;
|
||||
onload?: () => void;
|
||||
onerror?: (error: CATType.FileStorageError) => void;
|
||||
// public?: boolean;
|
||||
}
|
||||
): void;
|
||||
declare function CAT_fileStorage(
|
||||
action: "upload",
|
||||
details: {
|
||||
path: string;
|
||||
// 基础目录,如果未设置,则将脚本uuid作为目录
|
||||
baseDir?: string;
|
||||
data: Blob;
|
||||
onload?: () => void;
|
||||
// onprogress?: (progress: number) => void;
|
||||
onerror?: (error: CATType.FileStorageError) => void;
|
||||
// public?: boolean;
|
||||
}
|
||||
): void;
|
||||
declare function CAT_fileStorage(action: "config"): void;
|
||||
|
||||
/**
|
||||
* 脚本猫后台脚本重试, 当你的脚本出现错误时, 可以reject返回此错误, 以便脚本猫重试
|
||||
* 重试时间请注意不要与脚本执行时间冲突, 否则可能会导致重复执行, 最小重试时间为5s
|
||||
* @class CATRetryError
|
||||
*/
|
||||
declare class CATRetryError {
|
||||
/**
|
||||
* constructor 构造函数
|
||||
* @param {string} message 错误信息
|
||||
* @param {number} seconds x秒后重试, 单位秒
|
||||
*/
|
||||
constructor(message: string, seconds: number);
|
||||
|
||||
/**
|
||||
* constructor 构造函数
|
||||
* @param {string} message 错误信息
|
||||
* @param {Date} date 重试时间, 指定时间后重试
|
||||
*/
|
||||
constructor(message: string, date: Date);
|
||||
}
|
||||
|
||||
declare namespace CATType {
|
||||
interface ProxyRule {
|
||||
proxyServer: ProxyServer;
|
||||
matchUrl: string[];
|
||||
}
|
||||
|
||||
type ProxyScheme = "http" | "https" | "quic" | "socks4" | "socks5";
|
||||
|
||||
interface ProxyServer {
|
||||
scheme?: ProxyScheme;
|
||||
host: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
interface FileStorageError {
|
||||
// 错误码 -1 未知错误 1 用户未配置文件储存源 2 文件储存源配置错误 3 路径不存在
|
||||
// 4 上传失败 5 下载失败 6 删除失败 7 不允许的文件路径 8 网络类型的错误
|
||||
code: -1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface FileStorageFileInfo {
|
||||
// 文件名
|
||||
name: string;
|
||||
// 文件路径
|
||||
path: string;
|
||||
// 储存空间绝对路径
|
||||
absPath: string;
|
||||
// 文件大小
|
||||
size: number;
|
||||
// 文件摘要
|
||||
digest: string;
|
||||
// 文件创建时间
|
||||
createtime: number;
|
||||
// 文件修改时间
|
||||
updatetime: number;
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace GMTypes {
|
||||
/*
|
||||
* store为获取隐身窗口之类的cookie,这是一个实验性质的API,后续可能会改变
|
||||
*/
|
||||
type CookieAction = "list" | "delete" | "set" | "store";
|
||||
|
||||
type LoggerLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
type LoggerLabel = {
|
||||
[key: string]: string | boolean | number | undefined;
|
||||
};
|
||||
|
||||
interface CookieDetails {
|
||||
url?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
domain?: string;
|
||||
path?: string;
|
||||
secure?: boolean;
|
||||
session?: boolean;
|
||||
storeId?: string;
|
||||
httpOnly?: boolean;
|
||||
expirationDate?: number;
|
||||
// store用
|
||||
tabId?: number;
|
||||
}
|
||||
|
||||
interface Cookie {
|
||||
domain: string;
|
||||
name: string;
|
||||
storeId: string;
|
||||
value: string;
|
||||
session: boolean;
|
||||
hostOnly: boolean;
|
||||
expirationDate?: number;
|
||||
path: string;
|
||||
httpOnly: boolean;
|
||||
secure: boolean;
|
||||
}
|
||||
|
||||
// tabid是只有后台脚本监听才有的参数
|
||||
type ValueChangeListener = (
|
||||
name: string,
|
||||
oldValue: unknown,
|
||||
newValue: unknown,
|
||||
remote: boolean,
|
||||
tabid?: number
|
||||
) => unknown;
|
||||
|
||||
interface OpenTabOptions {
|
||||
active?: boolean;
|
||||
insert?: boolean;
|
||||
setParent?: boolean;
|
||||
useOpen?: boolean; // 这是一个实验性/不兼容其他管理器/不兼容Firefox的功能
|
||||
}
|
||||
|
||||
interface XHRResponse {
|
||||
finalUrl?: string;
|
||||
readyState?: 0 | 1 | 2 | 3 | 4;
|
||||
responseHeaders?: string;
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
response?: string | Blob | ArrayBuffer | Document | ReadableStream | null;
|
||||
responseText?: string;
|
||||
responseXML?: Document | null;
|
||||
responseType?: "text" | "arraybuffer" | "blob" | "json" | "document" | "stream";
|
||||
}
|
||||
|
||||
interface XHRProgress extends XHRResponse {
|
||||
done: number;
|
||||
lengthComputable: boolean;
|
||||
loaded: number;
|
||||
position?: number;
|
||||
total: number;
|
||||
totalSize: number;
|
||||
}
|
||||
|
||||
type Listener<OBJ> = (event: OBJ) => unknown;
|
||||
type ContextType = unknown;
|
||||
|
||||
interface XHRDetails {
|
||||
method?: "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
|
||||
url: string;
|
||||
headers?: { [key: string]: string };
|
||||
data?: string | FormData | Blob;
|
||||
cookie?: string;
|
||||
binary?: boolean;
|
||||
timeout?: number;
|
||||
context?: ContextType;
|
||||
responseType?: "text" | "arraybuffer" | "blob" | "json" | "document" | "stream"; // stream 在当前版本是一个较为简陋的实现
|
||||
overrideMimeType?: string;
|
||||
anonymous?: boolean;
|
||||
fetch?: boolean;
|
||||
user?: string;
|
||||
password?: string;
|
||||
nocache?: boolean;
|
||||
maxRedirects?: number;
|
||||
|
||||
onload?: Listener<XHRResponse>;
|
||||
onloadstart?: Listener<XHRResponse>;
|
||||
onloadend?: Listener<XHRResponse>;
|
||||
onprogress?: Listener<XHRProgress>;
|
||||
onreadystatechange?: Listener<XHRResponse>;
|
||||
ontimeout?: () => void;
|
||||
onabort?: () => void;
|
||||
onerror?: (err: string) => void;
|
||||
}
|
||||
|
||||
interface AbortHandle<RETURN_TYPE> {
|
||||
abort(): RETURN_TYPE;
|
||||
}
|
||||
|
||||
interface DownloadError {
|
||||
error: "not_enabled" | "not_whitelisted" | "not_permitted" | "not_supported" | "not_succeeded" | "unknown";
|
||||
details?: string;
|
||||
}
|
||||
|
||||
interface DownloadDetails {
|
||||
method?: "GET" | "POST";
|
||||
url: string;
|
||||
name: string;
|
||||
headers?: { [key: string]: string };
|
||||
saveAs?: boolean;
|
||||
timeout?: number;
|
||||
cookie?: string;
|
||||
anonymous?: boolean;
|
||||
|
||||
onerror?: Listener<DownloadError>;
|
||||
ontimeout?: () => void;
|
||||
onload?: Listener<object>;
|
||||
onprogress?: Listener<XHRProgress>;
|
||||
}
|
||||
|
||||
interface NotificationThis extends NotificationDetails {
|
||||
id: string;
|
||||
}
|
||||
|
||||
type NotificationOnClick = (this: NotificationThis, id: string, index?: number) => unknown;
|
||||
type NotificationOnDone = (this: NotificationThis, user: boolean) => unknown;
|
||||
|
||||
interface NotificationButton {
|
||||
title: string;
|
||||
iconUrl?: string;
|
||||
}
|
||||
|
||||
interface NotificationDetails {
|
||||
text?: string;
|
||||
title?: string;
|
||||
image?: string;
|
||||
highlight?: boolean;
|
||||
silent?: boolean;
|
||||
timeout?: number;
|
||||
onclick?: NotificationOnClick;
|
||||
ondone?: NotificationOnDone;
|
||||
progress?: number;
|
||||
oncreate?: NotificationOnClick;
|
||||
buttons?: NotificationButton[];
|
||||
}
|
||||
|
||||
interface Tab {
|
||||
close(): void;
|
||||
|
||||
onclose?: () => void;
|
||||
closed?: boolean;
|
||||
name?: string;
|
||||
}
|
||||
}
|
3
src/types/main.d.ts
vendored
3
src/types/main.d.ts
vendored
@@ -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 {
|
||||
|
26
src/types/scriptcat.d.ts
vendored
26
src/types/scriptcat.d.ts
vendored
@@ -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 {
|
||||
|
Reference in New Issue
Block a user