This commit is contained in:
王一之 2025-04-16 14:01:26 +08:00
parent 071e728f06
commit 1a531dfad5
11 changed files with 259 additions and 35 deletions

View File

@ -1,5 +1,6 @@
import { ExtServer, ExtServerApi } from "@App/app/const"; import { ExtServer, ExtServerApi } from "@App/app/const";
import { WarpTokenError } from "./error"; import { WarpTokenError } from "./error";
import { LocalStorageDAO } from "@App/app/repo/localStorage";
type NetDiskType = "baidu" | "onedrive"; type NetDiskType = "baidu" | "onedrive";
@ -8,11 +9,7 @@ export function GetNetDiskToken(netDiskType: NetDiskType): Promise<{
msg: string; msg: string;
data: { token: { access_token: string; refresh_token: string } }; data: { token: { access_token: string; refresh_token: string } };
}> { }> {
return fetch(ExtServerApi + `auth/net-disk/token?netDiskType=${netDiskType}`) return fetch(ExtServerApi + `auth/net-disk/token?netDiskType=${netDiskType}`).then((resp) => resp.json());
.then((resp) => resp.json())
.then((resp) => {
return resp.data;
});
} }
export function RefreshToken( export function RefreshToken(
@ -32,27 +29,45 @@ export function RefreshToken(
netDiskType, netDiskType,
refreshToken, refreshToken,
}), }),
}) }).then((resp) => resp.json());
.then((resp) => resp.json())
.then((resp) => {
return resp.data;
});
} }
export function NetDisk(netDiskType: NetDiskType) { export function NetDisk(netDiskType: NetDiskType) {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
const loginWindow = window.open(`${ExtServer}api/v1/auth/net-disk?netDiskType=${netDiskType}`); if (globalThis.window) {
const t = setInterval(() => { const loginWindow = window.open(`${ExtServer}api/v1/auth/net-disk?netDiskType=${netDiskType}`);
try { const t = setInterval(() => {
if (loginWindow!.closed) { try {
if (loginWindow!.closed) {
clearInterval(t);
resolve();
}
} catch (e) {
clearInterval(t); clearInterval(t);
resolve(); resolve();
} }
} catch (e) { }, 1000);
clearInterval(t); } else {
resolve(); chrome.tabs
} .create({
}, 1000); 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);
});
}
}); });
} }
@ -64,8 +79,15 @@ export type Token = {
export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) { export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) {
let token: Token | undefined; let token: Token | undefined;
const localStorageDao = new LocalStorageDAO();
const key = `netdisk:token:${netDiskType}`;
try { try {
token = JSON.parse(localStorage[`netdisk:token:${netDiskType}`]); token = await localStorageDao.get(key).then((resp) => {
if (resp) {
return resp.value;
}
return undefined;
});
} catch (e) { } catch (e) {
// ignore // ignore
} }
@ -83,7 +105,10 @@ export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) {
createtime: Date.now(), createtime: Date.now(),
}; };
invalid = false; invalid = false;
localStorage[`netdisk:token:${netDiskType}`] = JSON.stringify(token); await localStorageDao.save({
key,
value: token,
});
} }
// token过期或者失效 // token过期或者失效
if (Date.now() >= token.createtime + 3600000 || invalid) { if (Date.now() >= token.createtime + 3600000 || invalid) {
@ -91,7 +116,7 @@ export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) {
try { try {
const resp = await RefreshToken(netDiskType, token.refreshToken); const resp = await RefreshToken(netDiskType, token.refreshToken);
if (resp.code !== 0) { if (resp.code !== 0) {
localStorage.removeItem(`netdisk:token:${netDiskType}`); await localStorageDao.delete(key);
// 刷新失败,并且标记失效,尝试重新获取token // 刷新失败,并且标记失效,尝试重新获取token
if (invalid) { if (invalid) {
return AuthVerify(netDiskType); return AuthVerify(netDiskType);
@ -103,7 +128,11 @@ export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) {
refreshToken: resp.data.token.refresh_token, refreshToken: resp.data.token.refresh_token,
createtime: Date.now(), createtime: Date.now(),
}; };
localStorage[`netdisk:token:${netDiskType}`] = JSON.stringify(token); // 更新token
await localStorageDao.save({
key,
value: token,
});
} catch (e) { } catch (e) {
// 报错返回原token // 报错返回原token
return Promise.resolve(token.accessToken); return Promise.resolve(token.accessToken);

View File

@ -55,12 +55,11 @@ export default class BaiduFileSystem implements FileSystem {
}); });
} }
// eslint-disable-next-line no-undef async request(url: string, config?: RequestInit) {
request(url: string, config?: RequestInit) {
config = config || {}; config = config || {};
const headers = <Headers>config.headers || new Headers(); const headers = <Headers>config.headers || new Headers();
// 处理请求匿名不发送cookie // 处理请求匿名不发送cookie
chrome.declarativeNetRequest.updateDynamicRules({ await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [100], removeRuleIds: [100],
addRules: [ addRules: [
{ {

View File

@ -1,5 +1,3 @@
/* eslint-disable max-classes-per-file */
/* eslint-disable import/prefer-default-export */
import { calculateMd5 } from "@App/pkg/utils/utils"; import { calculateMd5 } from "@App/pkg/utils/utils";
import { MD5 } from "crypto-js"; import { MD5 } from "crypto-js";
import { File, FileReader, FileWriter } from "../filesystem"; import { File, FileReader, FileWriter } from "../filesystem";

View File

@ -1,4 +1,3 @@
// eslint-disable-next-line import/prefer-default-export, max-classes-per-file
export class WarpTokenError { export class WarpTokenError {
error: Error; error: Error;

View 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);
}
}

View File

@ -318,6 +318,52 @@ export default class GMApi {
this.sendMessage("GM_unregisterMenuCommand", [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指定允许的域名 // 用于脚本跨域请求,需要@connect domain指定允许的域名
@GMContext.API({ @GMContext.API({
depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"], depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"],

View File

@ -10,10 +10,13 @@ import EventEmitter from "eventemitter3";
import { MessageQueue } from "@Packages/message/message_queue"; import { MessageQueue } from "@Packages/message/message_queue";
import { RuntimeService } from "./runtime"; import { RuntimeService } from "./runtime";
import { getIcon, isFirefox } from "@App/pkg/utils/utils"; import { getIcon, isFirefox } from "@App/pkg/utils/utils";
import { PopupService } from "./popup";
import { act } from "react";
import { MockMessageConnect } from "@Packages/message/mock_message"; import { MockMessageConnect } from "@Packages/message/mock_message";
import i18next, { i18nName } from "@App/locales/locales"; 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调用请求 // GMApi,处理脚本的GM API调用请求
@ -71,6 +74,7 @@ export default class GMApi {
scriptDAO: ScriptDAO = new ScriptDAO(); scriptDAO: ScriptDAO = new ScriptDAO();
constructor( constructor(
private systemConfig: SystemConfig,
private permissionVerify: PermissionVerify, private permissionVerify: PermissionVerify,
private group: Group, private group: Group,
private send: MessageSend, private send: MessageSend,
@ -248,6 +252,119 @@ 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规则 // 根据header生成dnr规则
async buildDNRRule(reqeustId: number, params: GMSend.XHRDetails): Promise<{ [key: string]: string }> { async buildDNRRule(reqeustId: number, params: GMSend.XHRDetails): Promise<{ [key: string]: string }> {
// 检查是否有unsafe header,有则生成dnr规则 // 检查是否有unsafe header,有则生成dnr规则
@ -401,7 +518,6 @@ export default class GMApi {
@PermissionVerify.API({ @PermissionVerify.API({
confirm: async (request: Request) => { confirm: async (request: Request) => {
console.log("confirm", request);
const config = <GMSend.XHRDetails>request.params[0]; const config = <GMSend.XHRDetails>request.params[0];
const url = new URL(config.url); const url = new URL(config.url);
if (request.script.metadata.connect) { if (request.script.metadata.connect) {

View File

@ -6,6 +6,7 @@ import { ValueService } from "./value";
import { RuntimeService } from "./runtime"; import { RuntimeService } from "./runtime";
import { ServiceWorkerMessageSend } from "@Packages/message/window_message"; import { ServiceWorkerMessageSend } from "@Packages/message/window_message";
import { PopupService } from "./popup"; import { PopupService } from "./popup";
import { SystemConfig } from "@App/pkg/config/config";
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode"; export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
@ -24,12 +25,14 @@ export default class ServiceWorkerManager {
this.mq.emit("preparationOffscreen", {}); this.mq.emit("preparationOffscreen", {});
}); });
const systemConfig = new SystemConfig(this.mq);
const resource = new ResourceService(this.api.group("resource"), this.mq); const resource = new ResourceService(this.api.group("resource"), this.mq);
resource.init(); resource.init();
const value = new ValueService(this.api.group("value"), this.sender); 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(this.api.group("script"), this.mq, value, resource);
script.init(); 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(); runtime.init();
const popup = new PopupService(this.api.group("popup"), this.mq, runtime); const popup = new PopupService(this.api.group("popup"), this.mq, runtime);
popup.init(); popup.init();

View File

@ -181,7 +181,6 @@ export default class PermissionVerify {
} }
return Promise.resolve(model); return Promise.resolve(model);
}); });
console.log("confirm", request, confirm);
// 有查询到结果,进入判断,不再需要用户确认 // 有查询到结果,进入判断,不再需要用户确认
if (ret) { if (ret) {
if (ret.allow) { if (ret.allow) {

View File

@ -25,6 +25,7 @@ import { PopupService } from "./popup";
import Logger from "@App/app/logger/logger"; import Logger from "@App/app/logger/logger";
import LoggerCore from "@App/app/logger/core"; import LoggerCore from "@App/app/logger/core";
import PermissionVerify from "./permission_verify"; import PermissionVerify from "./permission_verify";
import { SystemConfig } from "@App/pkg/config/config";
// 为了优化性能存储到缓存时删除了code与value // 为了优化性能存储到缓存时删除了code与value
export interface ScriptMatchInfo extends ScriptRunResouce { export interface ScriptMatchInfo extends ScriptRunResouce {
@ -48,6 +49,7 @@ export class RuntimeService {
scriptMatchCache: Map<string, ScriptMatchInfo> | null | undefined; scriptMatchCache: Map<string, ScriptMatchInfo> | null | undefined;
constructor( constructor(
private systemConfig: SystemConfig,
private group: Group, private group: Group,
private sender: MessageSend, private sender: MessageSend,
private mq: MessageQueue, private mq: MessageQueue,
@ -58,7 +60,7 @@ export class RuntimeService {
async init() { async init() {
// 启动gm api // 启动gm api
const permission = new PermissionVerify(this.group.group("permission")); const permission = new PermissionVerify(this.group.group("permission"));
const gmApi = new GMApi(permission, this.group, this.sender, this.mq, this.value, this); const gmApi = new GMApi(this.systemConfig, permission, this.group, this.sender, this.mq, this.value, this);
permission.init(); permission.init();
gmApi.start(); gmApi.start();

View File

@ -1,5 +1,6 @@
import { Metadata, Script } from "@App/app/repo/scripts"; import { Metadata, Script } from "@App/app/repo/scripts";
import { CronTime } from "cron"; import { CronTime } from "cron";
import crypto from "crypto-js";
import dayjs from "dayjs"; import dayjs from "dayjs";
import semver from "semver"; import semver from "semver";
@ -234,3 +235,18 @@ export function getIcon(script: Script): string | undefined {
(script.metadata.icon64url && script.metadata.icon64url[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());
}
};
});
}