mv3
This commit is contained in:
@ -16,13 +16,17 @@
|
||||
"lint-fix": "eslint --fix ."
|
||||
},
|
||||
"dependencies": {
|
||||
"cron": "^3.2.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dexie": "^4.0.10",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"i18next": "^23.16.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^15.1.0",
|
||||
"uuid": "^11.0.3"
|
||||
"semver": "^7.6.3",
|
||||
"uuid": "^11.0.3",
|
||||
"yaml": "^2.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.0",
|
||||
@ -32,6 +36,7 @@
|
||||
"@types/chrome": "^0.0.279",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@vitest/coverage-v8": "2.1.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.12.0",
|
||||
|
51
pnpm-lock.yaml
generated
51
pnpm-lock.yaml
generated
@ -8,9 +8,15 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
cron:
|
||||
specifier: ^3.2.1
|
||||
version: 3.2.1
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
dexie:
|
||||
specifier: ^4.0.10
|
||||
version: 4.0.10
|
||||
eventemitter3:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1
|
||||
@ -26,9 +32,15 @@ importers:
|
||||
react-i18next:
|
||||
specifier: ^15.1.0
|
||||
version: 15.1.0(i18next@23.16.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
semver:
|
||||
specifier: ^7.6.3
|
||||
version: 7.6.3
|
||||
uuid:
|
||||
specifier: ^11.0.3
|
||||
version: 11.0.3
|
||||
yaml:
|
||||
specifier: ^2.6.1
|
||||
version: 2.6.1
|
||||
devDependencies:
|
||||
'@eslint/compat':
|
||||
specifier: ^1.2.0
|
||||
@ -51,6 +63,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: ^18.2.18
|
||||
version: 18.3.1
|
||||
'@types/semver':
|
||||
specifier: ^7.5.8
|
||||
version: 7.5.8
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 2.1.4
|
||||
version: 2.1.4(vitest@2.1.4(@types/node@22.8.1)(jsdom@25.0.1))
|
||||
@ -767,6 +782,9 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/luxon@3.4.2':
|
||||
resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==}
|
||||
|
||||
'@types/mime@1.3.5':
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
||||
@ -797,6 +815,9 @@ packages:
|
||||
'@types/retry@0.12.2':
|
||||
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
|
||||
|
||||
'@types/semver@7.5.8':
|
||||
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
|
||||
|
||||
'@types/send@0.17.4':
|
||||
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
|
||||
|
||||
@ -1152,6 +1173,9 @@ packages:
|
||||
create-require@1.1.1:
|
||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||
|
||||
cron@3.2.1:
|
||||
resolution: {integrity: sha512-w2n5l49GMmmkBFEsH9FIDhjZ1n1QgTMOCMGuQtOXs5veNiosZmso6bQGuqOJSYAXXrG84WQFVneNk+Yt0Ua9iw==}
|
||||
|
||||
cross-env@7.0.3:
|
||||
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
|
||||
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
|
||||
@ -1257,6 +1281,9 @@ packages:
|
||||
detect-node@2.1.0:
|
||||
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
|
||||
|
||||
dexie@4.0.10:
|
||||
resolution: {integrity: sha512-eM2RzuR3i+M046r2Q0Optl3pS31qTWf8aFuA7H9wnsHTwl8EPvroVLwvQene/6paAs39Tbk6fWZcn2aZaHkc/w==}
|
||||
|
||||
diff@4.0.2:
|
||||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
@ -1937,6 +1964,10 @@ packages:
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
luxon@3.5.0:
|
||||
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
magic-string@0.30.12:
|
||||
resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==}
|
||||
|
||||
@ -2881,6 +2912,11 @@ packages:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yaml@2.6.1:
|
||||
resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==}
|
||||
engines: {node: '>= 14'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
@ -3373,6 +3409,8 @@ snapshots:
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/luxon@3.4.2': {}
|
||||
|
||||
'@types/mime@1.3.5': {}
|
||||
|
||||
'@types/node-forge@1.3.11':
|
||||
@ -3402,6 +3440,8 @@ snapshots:
|
||||
|
||||
'@types/retry@0.12.2': {}
|
||||
|
||||
'@types/semver@7.5.8': {}
|
||||
|
||||
'@types/send@0.17.4':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
@ -3841,6 +3881,11 @@ snapshots:
|
||||
|
||||
create-require@1.1.1: {}
|
||||
|
||||
cron@3.2.1:
|
||||
dependencies:
|
||||
'@types/luxon': 3.4.2
|
||||
luxon: 3.5.0
|
||||
|
||||
cross-env@7.0.3:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.3
|
||||
@ -3931,6 +3976,8 @@ snapshots:
|
||||
|
||||
detect-node@2.1.0: {}
|
||||
|
||||
dexie@4.0.10: {}
|
||||
|
||||
diff@4.0.2: {}
|
||||
|
||||
dns-packet@5.6.1:
|
||||
@ -4756,6 +4803,8 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
luxon@3.5.0: {}
|
||||
|
||||
magic-string@0.30.12:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
@ -5804,6 +5853,8 @@ snapshots:
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yaml@2.6.1: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.6.2:
|
||||
|
@ -39,6 +39,9 @@ export default defineConfig({
|
||||
"@App": path.resolve(__dirname, "src/"),
|
||||
"@Packages": path.resolve(__dirname, "packages/"),
|
||||
},
|
||||
fallback: {
|
||||
child_process: false,
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
|
42
src/app/cache.ts
Normal file
42
src/app/cache.ts
Normal file
@ -0,0 +1,42 @@
|
||||
export default class Cache {
|
||||
static instance: Cache = new Cache();
|
||||
|
||||
static getInstance(): Cache {
|
||||
return Cache.instance;
|
||||
}
|
||||
|
||||
map: Map<string, unknown>;
|
||||
|
||||
private constructor() {
|
||||
this.map = new Map<string, unknown>();
|
||||
}
|
||||
|
||||
public get(key: string): unknown {
|
||||
return this.map.get(key);
|
||||
}
|
||||
|
||||
public async getOrSet(key: string, set: () => Promise<unknown>): Promise<unknown> {
|
||||
let ret = this.get(key);
|
||||
if (!ret) {
|
||||
ret = await set();
|
||||
this.set(key, ret);
|
||||
}
|
||||
return Promise.resolve(ret);
|
||||
}
|
||||
|
||||
public set(key: string, value: unknown): void {
|
||||
this.map.set(key, value);
|
||||
}
|
||||
|
||||
public has(key: string): boolean {
|
||||
return this.map.has(key);
|
||||
}
|
||||
|
||||
public del(key: string): void {
|
||||
this.map.delete(key);
|
||||
}
|
||||
|
||||
public list(): string[] {
|
||||
return Array.from(this.map.keys());
|
||||
}
|
||||
}
|
43
src/app/cache_key.ts
Normal file
43
src/app/cache_key.ts
Normal file
@ -0,0 +1,43 @@
|
||||
// 缓存key,所有缓存相关的key都需要定义在此
|
||||
// 使用装饰器维护缓存值
|
||||
import { ConfirmParam } from "@App/runtime/background/permission_verify";
|
||||
|
||||
export default class CacheKey {
|
||||
// 缓存触发器
|
||||
static Trigger(): (target: unknown, propertyName: string, descriptor: PropertyDescriptor) => void {
|
||||
return (target, propertyName, descriptor) => {
|
||||
descriptor.value();
|
||||
};
|
||||
}
|
||||
|
||||
// 脚本缓存
|
||||
static script(id: number): string {
|
||||
return `script:${id.toString()}`;
|
||||
}
|
||||
|
||||
// 加载脚本信息时的缓存,已处理删除
|
||||
static scriptInfo(uuid: string): string {
|
||||
return `scriptInfo:${uuid}`;
|
||||
}
|
||||
|
||||
// 脚本资源url缓存,可能存在泄漏
|
||||
static resourceByUrl(url: string): string {
|
||||
return `resource:${url}`;
|
||||
}
|
||||
|
||||
// 脚本value缓存,可能存在泄漏
|
||||
static scriptValue(id: number, storagename?: string[]): string {
|
||||
if (storagename) {
|
||||
return `value:storagename:${storagename[0]}`;
|
||||
}
|
||||
return `value:id:${id.toString()}`;
|
||||
}
|
||||
|
||||
static permissionConfirm(scriptId: number, confirm: ConfirmParam): string {
|
||||
return `permission:${scriptId.toString()}:${confirm.permissionValue || ""}:${confirm.permission || ""}`;
|
||||
}
|
||||
|
||||
static importInfo(uuid: string): string {
|
||||
return `import:${uuid}`;
|
||||
}
|
||||
}
|
50
src/app/logger/core.ts
Normal file
50
src/app/logger/core.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import EventEmitter from "eventemitter3";
|
||||
import Logger from "./logger";
|
||||
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
export interface LogLabel {
|
||||
[key: string]: string | string[] | boolean | number | undefined;
|
||||
component?: string;
|
||||
}
|
||||
|
||||
// 储存
|
||||
export interface Writer {
|
||||
write(level: LogLevel, message: string, label: LogLabel): void;
|
||||
}
|
||||
|
||||
export default class LoggerCore {
|
||||
static instance: LoggerCore;
|
||||
|
||||
static getInstance() {
|
||||
return LoggerCore.instance;
|
||||
}
|
||||
|
||||
static getLogger(...label: LogLabel[]) {
|
||||
return LoggerCore.getInstance().logger(...label);
|
||||
}
|
||||
|
||||
writer: Writer;
|
||||
|
||||
level: LogLevel = "info";
|
||||
|
||||
debug: boolean = false;
|
||||
|
||||
labels: LogLabel;
|
||||
|
||||
constructor(config: { level?: LogLevel; debug?: boolean; writer: Writer; labels: LogLabel }) {
|
||||
this.writer = config.writer;
|
||||
this.level = config.level || this.level;
|
||||
this.debug = config.debug || this.debug;
|
||||
this.labels = config.labels || {};
|
||||
if (!LoggerCore.instance) {
|
||||
LoggerCore.instance = this;
|
||||
}
|
||||
}
|
||||
|
||||
logger(...label: LogLabel[]) {
|
||||
return new Logger(this, this.labels, ...label);
|
||||
}
|
||||
|
||||
static EE = new EventEmitter();
|
||||
}
|
21
src/app/logger/db_writer.ts
Normal file
21
src/app/logger/db_writer.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { LoggerDAO } from "../repo/logger";
|
||||
import { LogLabel, LogLevel, Writer } from "./core";
|
||||
|
||||
// 使用indexdb作为日志存储
|
||||
export default class DBWriter implements Writer {
|
||||
dao: LoggerDAO;
|
||||
|
||||
constructor(dao: LoggerDAO) {
|
||||
this.dao = dao;
|
||||
}
|
||||
|
||||
write(level: LogLevel, message: string, label: LogLabel): void {
|
||||
this.dao.save({
|
||||
id: 0,
|
||||
level,
|
||||
message,
|
||||
label,
|
||||
createtime: new Date().getTime(),
|
||||
});
|
||||
}
|
||||
}
|
92
src/app/logger/logger.ts
Normal file
92
src/app/logger/logger.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/* eslint-disable no-console */
|
||||
import dayjs from "dayjs";
|
||||
import LoggerCore, { LogLabel, LogLevel } from "./core";
|
||||
|
||||
const levelNumber = {
|
||||
debug: 10,
|
||||
info: 100,
|
||||
warn: 1000,
|
||||
error: 10000,
|
||||
};
|
||||
|
||||
function buildLabel(...label: LogLabel[][]): LogLabel {
|
||||
const ret: LogLabel = {};
|
||||
label.forEach((item) => {
|
||||
item.forEach((item2) => {
|
||||
Object.keys(item2).forEach((key) => {
|
||||
ret[key] = item2[key];
|
||||
});
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
export default class Logger {
|
||||
core: LoggerCore;
|
||||
|
||||
label: LogLabel[];
|
||||
|
||||
constructor(core: LoggerCore, ...label: LogLabel[]) {
|
||||
this.core = core;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
log(level: LogLevel, message: string, ...label: LogLabel[]) {
|
||||
if (levelNumber[level] >= levelNumber[this.core.level]) {
|
||||
this.core.writer.write(level, message, buildLabel(this.label, label));
|
||||
}
|
||||
if (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)
|
||||
)}`;
|
||||
switch (level) {
|
||||
case "error":
|
||||
console.error(msg);
|
||||
break;
|
||||
case "warn":
|
||||
console.warn(msg);
|
||||
break;
|
||||
default:
|
||||
console.info(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
LoggerCore.EE.emit("log", { level, message, label });
|
||||
}
|
||||
|
||||
with(...label: LogLabel[]) {
|
||||
return new Logger(this.core, ...this.label, ...label);
|
||||
}
|
||||
|
||||
debug(message: string, ...label: LogLabel[]) {
|
||||
this.log("debug", message, ...label);
|
||||
}
|
||||
|
||||
info(message: string, ...label: LogLabel[]) {
|
||||
this.log("info", message, ...label);
|
||||
}
|
||||
|
||||
warn(message: string, ...label: LogLabel[]) {
|
||||
this.log("warn", message, ...label);
|
||||
}
|
||||
|
||||
error(message: string, ...label: LogLabel[]) {
|
||||
this.log("error", message, ...label);
|
||||
}
|
||||
|
||||
static E(e: unknown): LogLabel {
|
||||
if (typeof e === "string") {
|
||||
return { error: e };
|
||||
}
|
||||
if (e instanceof Error) {
|
||||
return { error: e.message };
|
||||
}
|
||||
if (typeof e === "object") {
|
||||
return e as never;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
29
src/app/logger/message_writer.ts
Normal file
29
src/app/logger/message_writer.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import MessageCenter from "../message/center";
|
||||
import { MessageManager } from "../message/message";
|
||||
import { Logger, LoggerDAO } from "../repo/logger";
|
||||
import { LogLabel, LogLevel, Writer } from "./core";
|
||||
|
||||
// 通过通讯机制写入日志
|
||||
export default class MessageWriter implements Writer {
|
||||
connect: MessageManager;
|
||||
|
||||
constructor(connect: MessageManager) {
|
||||
this.connect = connect;
|
||||
}
|
||||
|
||||
write(level: LogLevel, message: string, label: LogLabel): void {
|
||||
this.connect.send("log", {
|
||||
id: 0,
|
||||
level,
|
||||
message,
|
||||
label,
|
||||
createtime: new Date().getTime(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function ListenerMessage(db: LoggerDAO, connect: MessageCenter) {
|
||||
connect.setHandler("log", (action, data: Logger) => {
|
||||
db.save(data);
|
||||
});
|
||||
}
|
112
src/app/migrate.ts
Normal file
112
src/app/migrate.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { db } from "./repo/dao";
|
||||
import { Script } from "./repo/scripts";
|
||||
|
||||
// 0.10.0重构,重命名字段,统一使用小峰驼
|
||||
function renameField(): void {
|
||||
db.version(16)
|
||||
.stores({
|
||||
scripts:
|
||||
"++id,&uuid,name,namespace,author,originDomain,subscribeUrl,type,sort,status," +
|
||||
"runStatus,createtime,updatetime,checktime",
|
||||
logger: "++id,level,createtime",
|
||||
// export: "++id,&scriptId",
|
||||
})
|
||||
.upgrade(async (tx) => {
|
||||
await tx.table("export").clear();
|
||||
return tx
|
||||
.table("scripts")
|
||||
.toCollection()
|
||||
.modify((script: { [key: string]: any }) => {
|
||||
if (script.origin_domain) {
|
||||
script.originDomain = script.origin_domain;
|
||||
}
|
||||
if (script.checkupdate_url) {
|
||||
script.checkUpdateUrl = script.checkupdate_url;
|
||||
}
|
||||
if (script.download_url) {
|
||||
script.downloadUrl = script.download_url;
|
||||
}
|
||||
});
|
||||
});
|
||||
db.version(17).stores({
|
||||
// export是0.10.x时的兼容性处理
|
||||
export: "++id,&scriptId",
|
||||
});
|
||||
}
|
||||
|
||||
export default function migrate() {
|
||||
// 数据库索引定义,每一次变动必须更新version
|
||||
db.version(1).stores({
|
||||
scripts: "++id,&uuid,name,namespace,author,origin_domain,type,status,createtime,updatetime,checktime",
|
||||
});
|
||||
db.version(2).stores({
|
||||
logger: "++id,level,origin,createtime",
|
||||
permission: "++id,[scriptId+permission+permissionValue],createtime,updatetime",
|
||||
});
|
||||
db.version(3).stores({
|
||||
logger: "++id,level,title,origin,createtime",
|
||||
});
|
||||
db.version(4).stores({
|
||||
value: "++id,scriptId,namespace,key,createtime",
|
||||
});
|
||||
db.version(5).stores({
|
||||
logger: "++id,level,origin,createtime,title,[origin+title],[level+origin+title]",
|
||||
});
|
||||
db.version(6).stores({
|
||||
scripts: "++id,&uuid,name,namespace,author,origin_domain,type,status,runStatus,createtime,updatetime,checktime",
|
||||
});
|
||||
db.version(7).stores({
|
||||
resource: "++id,&url,content,type,createtime,updatetime",
|
||||
resourceLink: "++id,url,scriptId,createtime",
|
||||
});
|
||||
db.version(8).stores({
|
||||
logger: "++id,level,origin,createtime",
|
||||
});
|
||||
db.version(9).stores({
|
||||
logger: "++id,level,scriptId,origin,createtime",
|
||||
});
|
||||
db.version(10)
|
||||
.stores({
|
||||
scripts:
|
||||
"++id,&uuid,name,namespace,author,origin_domain,type,sort,status,runStatus,createtime,updatetime,checktime",
|
||||
})
|
||||
.upgrade((tx) => {
|
||||
return tx
|
||||
.table("scripts")
|
||||
.toCollection()
|
||||
.modify((script: Script) => {
|
||||
script.sort = 0;
|
||||
});
|
||||
});
|
||||
db.version(11).stores({
|
||||
export: "++id,&uuid,scriptId",
|
||||
});
|
||||
db.version(12)
|
||||
.stores({
|
||||
value: "++id,scriptId,storageName,key,createtime",
|
||||
})
|
||||
.upgrade((tx) => {
|
||||
tx.table("value")
|
||||
.toCollection()
|
||||
.modify((value) => {
|
||||
if (value.namespace) {
|
||||
value.storageName = value.namespace;
|
||||
delete value.namespace;
|
||||
}
|
||||
});
|
||||
});
|
||||
db.version(13).stores({
|
||||
subscribe: "++id,&url,createtime,updatetime,checktime",
|
||||
scripts:
|
||||
"++id,&uuid,name,namespace,author,origin_domain,subscribeUrl,type,sort,status,runStatus,createtime,updatetime,checktime",
|
||||
sync: "++id,&key,[user+device+type],createtime",
|
||||
});
|
||||
db.version(14).stores({
|
||||
value: "++id,[scriptId+key],[storageName+key]",
|
||||
});
|
||||
db.version(15).stores({
|
||||
permission: "++id,scriptId,[scriptId+permission+permissionValue],createtime,updatetime",
|
||||
});
|
||||
// 使用小峰驼统一命名规范
|
||||
renameField();
|
||||
}
|
62
src/app/repo/dao.test.ts
Normal file
62
src/app/repo/dao.test.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import "fake-indexeddb/auto";
|
||||
import { DAO, db } from "./dao";
|
||||
import { LoggerDAO } from "./logger";
|
||||
import migrate from "../migrate";
|
||||
|
||||
migrate();
|
||||
|
||||
interface Test {
|
||||
id: number;
|
||||
data: string;
|
||||
}
|
||||
|
||||
db.version(17).stores({ test: "++id,data" });
|
||||
|
||||
class testDAO extends DAO<Test> {
|
||||
public tableName = "test";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.table = db.table(this.tableName);
|
||||
}
|
||||
}
|
||||
|
||||
describe("dao", () => {
|
||||
const dao = new testDAO();
|
||||
it("测试save", async () => {
|
||||
expect(await dao.save({ id: 0, data: "ok1" })).toEqual(1);
|
||||
|
||||
expect(await dao.save({ id: 0, data: "ok" })).toEqual(2);
|
||||
|
||||
expect(await dao.save({ id: 2, data: "ok2" })).toEqual(2);
|
||||
});
|
||||
|
||||
it("测试find", async () => {
|
||||
expect(await dao.findOne({ id: 1 })).toEqual({ id: 1, data: "ok1" });
|
||||
expect(await dao.findById(2)).toEqual({ id: 2, data: "ok2" });
|
||||
});
|
||||
|
||||
it("测试list", async () => {
|
||||
expect(await dao.list({ id: 1 })).toEqual([{ id: 1, data: "ok1" }]);
|
||||
});
|
||||
|
||||
it("测试delete", async () => {
|
||||
expect(await dao.delete({ id: 1 })).toEqual(1);
|
||||
expect(await dao.findById(1)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("model", () => {
|
||||
const logger = new LoggerDAO();
|
||||
it("save", async () => {
|
||||
expect(
|
||||
await logger.save({
|
||||
id: 0,
|
||||
level: "info",
|
||||
message: "ok",
|
||||
label: {},
|
||||
createtime: new Date().getTime(),
|
||||
})
|
||||
).toEqual(1);
|
||||
});
|
||||
});
|
105
src/app/repo/dao.ts
Normal file
105
src/app/repo/dao.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import Dexie from "dexie";
|
||||
|
||||
export const db = new Dexie("ScriptCat");
|
||||
|
||||
export const ErrSaveError = new Error("数据保存失败");
|
||||
|
||||
export class Page {
|
||||
protected Page: number;
|
||||
|
||||
protected Count: number;
|
||||
|
||||
protected Order: string;
|
||||
|
||||
protected Sort: "asc" | "desc";
|
||||
|
||||
constructor(page: number, count: number, sort?: "asc" | "desc", order?: string) {
|
||||
this.Page = page;
|
||||
this.Count = count;
|
||||
this.Order = order || "id";
|
||||
this.Sort = sort || "desc";
|
||||
}
|
||||
|
||||
public page() {
|
||||
return this.Page;
|
||||
}
|
||||
|
||||
public count() {
|
||||
return this.Count;
|
||||
}
|
||||
|
||||
public order() {
|
||||
return this.Order;
|
||||
}
|
||||
|
||||
public sort() {
|
||||
return this.Sort;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class DAO<T> {
|
||||
public table!: Dexie.Table<T, number>;
|
||||
|
||||
public tableName = "";
|
||||
|
||||
public list(query: { [key: string]: any }, page?: Page) {
|
||||
if (!page) {
|
||||
return this.table.where(query).toArray();
|
||||
}
|
||||
let collect = this.table
|
||||
.where(query)
|
||||
.offset((page.page() - 1) * page.count())
|
||||
.limit(page.count());
|
||||
if (page.order() !== "id") {
|
||||
collect.sortBy(page.order());
|
||||
}
|
||||
if (page.sort() === "desc") {
|
||||
collect = collect.reverse();
|
||||
}
|
||||
return collect.toArray();
|
||||
}
|
||||
|
||||
public find() {
|
||||
return this.table;
|
||||
}
|
||||
|
||||
public findOne(where: { [key: string]: any }) {
|
||||
return this.table.where(where).first();
|
||||
}
|
||||
|
||||
public async save(val: T) {
|
||||
const id = <number>(<any>val).id;
|
||||
if (!id) {
|
||||
delete (<any>val).id;
|
||||
return this.table.add(val);
|
||||
}
|
||||
const resp = await this.table.update(id, <any>val);
|
||||
if (resp) {
|
||||
return Promise.resolve(id);
|
||||
}
|
||||
return Promise.reject(ErrSaveError);
|
||||
}
|
||||
|
||||
public findById(id: number) {
|
||||
return this.table.get(id);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
return this.table.clear();
|
||||
}
|
||||
|
||||
public async delete(id: number | { [key: string]: any }) {
|
||||
if (typeof id === "number") {
|
||||
return this.table.where({ id }).delete();
|
||||
}
|
||||
return this.table.where(id).delete();
|
||||
}
|
||||
|
||||
public update(id: number, changes: { [key: string]: any }) {
|
||||
return this.table.update(id, changes);
|
||||
}
|
||||
|
||||
public count() {
|
||||
return this.table.count();
|
||||
}
|
||||
}
|
28
src/app/repo/export.ts
Normal file
28
src/app/repo/export.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { ExportParams } from "@Pkg/cloudscript/cloudscript";
|
||||
import { DAO, db } from "./dao";
|
||||
|
||||
export type ExportTarget = "local" | "tencentCloud";
|
||||
|
||||
// 导出与本地脚本关联记录
|
||||
export interface Export {
|
||||
id: number;
|
||||
scriptId: number;
|
||||
params: {
|
||||
[key: string]: ExportParams;
|
||||
};
|
||||
// 导出目标
|
||||
target: ExportTarget;
|
||||
}
|
||||
|
||||
export class ExportDAO extends DAO<Export> {
|
||||
public tableName = "export";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.table = db.table(this.tableName);
|
||||
}
|
||||
|
||||
findByScriptID(scriptID: number) {
|
||||
return this.table.where({ scriptId: scriptID }).first();
|
||||
}
|
||||
}
|
32
src/app/repo/logger.ts
Normal file
32
src/app/repo/logger.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { LogLabel, LogLevel } from "../logger/core";
|
||||
import { DAO, db } from "./dao";
|
||||
|
||||
export interface Logger {
|
||||
id: number;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
label: LogLabel;
|
||||
createtime: number;
|
||||
}
|
||||
|
||||
export class LoggerDAO extends DAO<Logger> {
|
||||
public tableName = "logger";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.table = db.table(this.tableName);
|
||||
}
|
||||
|
||||
async queryLogs(startTime: number, endTime: number) {
|
||||
const ret = await this.table
|
||||
.where("createtime")
|
||||
.between(startTime, endTime)
|
||||
.toArray();
|
||||
|
||||
return ret.sort((a, b) => b.createtime - a.createtime);
|
||||
}
|
||||
|
||||
deleteBefore(time: number) {
|
||||
return this.table.where("createtime").below(time).delete();
|
||||
}
|
||||
}
|
20
src/app/repo/permission.ts
Normal file
20
src/app/repo/permission.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { DAO, db } from "./dao";
|
||||
|
||||
export interface Permission {
|
||||
id: number;
|
||||
scriptId: number;
|
||||
permission: string;
|
||||
permissionValue: string;
|
||||
allow: boolean;
|
||||
createtime: number;
|
||||
updatetime: number;
|
||||
}
|
||||
|
||||
export class PermissionDAO extends DAO<Permission> {
|
||||
public tableName = "permission";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.table = db.table(this.tableName);
|
||||
}
|
||||
}
|
32
src/app/repo/resource.ts
Normal file
32
src/app/repo/resource.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { DAO, db } from "./dao";
|
||||
|
||||
export type ResourceType = "require" | "require-css" | "resource";
|
||||
|
||||
export interface Resource {
|
||||
id: number;
|
||||
url: string;
|
||||
content: string;
|
||||
base64: string;
|
||||
hash: ResourceHash;
|
||||
type: ResourceType;
|
||||
contentType: string;
|
||||
createtime: number;
|
||||
updatetime?: number;
|
||||
}
|
||||
|
||||
export interface ResourceHash {
|
||||
md5: string;
|
||||
sha1: string;
|
||||
sha256: string;
|
||||
sha384: string;
|
||||
sha512: string;
|
||||
}
|
||||
|
||||
export class ResourceDAO extends DAO<Resource> {
|
||||
public tableName = "resource";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.table = db.table(this.tableName);
|
||||
}
|
||||
}
|
17
src/app/repo/resource_link.ts
Normal file
17
src/app/repo/resource_link.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { DAO, db } from "./dao";
|
||||
|
||||
export interface ResourceLink {
|
||||
id: number;
|
||||
url: string;
|
||||
scriptId: number;
|
||||
createtime?: number;
|
||||
}
|
||||
|
||||
export class ResourceLinkDAO extends DAO<ResourceLink> {
|
||||
public tableName = "resourceLink";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.table = db.table(this.tableName);
|
||||
}
|
||||
}
|
121
src/app/repo/scripts.ts
Normal file
121
src/app/repo/scripts.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { DAO, db } from "./dao";
|
||||
import { Resource } from "./resource";
|
||||
import { Value } from "./value";
|
||||
|
||||
// 脚本模型
|
||||
export type SCRIPT_TYPE = 1 | 2 | 3;
|
||||
|
||||
export const SCRIPT_TYPE_NORMAL: SCRIPT_TYPE = 1;
|
||||
export const SCRIPT_TYPE_CRONTAB: SCRIPT_TYPE = 2;
|
||||
export const SCRIPT_TYPE_BACKGROUND: SCRIPT_TYPE = 3;
|
||||
|
||||
export type SCRIPT_STATUS = 1 | 2 | 3 | 4;
|
||||
|
||||
export const SCRIPT_STATUS_ENABLE: SCRIPT_STATUS = 1;
|
||||
export const SCRIPT_STATUS_DISABLE: SCRIPT_STATUS = 2;
|
||||
// 弃用
|
||||
export const SCRIPT_STATUS_ERROR: SCRIPT_STATUS = 3;
|
||||
export const SCRIPT_STATUS_DELETE: SCRIPT_STATUS = 4;
|
||||
|
||||
export type SCRIPT_RUN_STATUS = "running" | "complete" | "error" | "retry";
|
||||
export const SCRIPT_RUN_STATUS_RUNNING: SCRIPT_RUN_STATUS = "running";
|
||||
export const SCRIPT_RUN_STATUS_COMPLETE: SCRIPT_RUN_STATUS = "complete";
|
||||
export const SCRIPT_RUN_STATUS_ERROR: SCRIPT_RUN_STATUS = "error";
|
||||
// 弃用
|
||||
export const SCRIPT_RUN_STATUS_RETRY: SCRIPT_RUN_STATUS = "retry";
|
||||
|
||||
export type Metadata = { [key: string]: string[] };
|
||||
|
||||
export type ConfigType =
|
||||
| "text"
|
||||
| "checkbox"
|
||||
| "select"
|
||||
| "mult-select"
|
||||
| "number"
|
||||
| "textarea"
|
||||
| "time";
|
||||
|
||||
export interface Config {
|
||||
[key: string]: any;
|
||||
title: string;
|
||||
description: string;
|
||||
default?: any;
|
||||
type?: ConfigType;
|
||||
bind?: string;
|
||||
values?: any[];
|
||||
password?: boolean;
|
||||
// 文本类型时是字符串长度,数字类型时是最大值
|
||||
max?: number;
|
||||
min?: number;
|
||||
rows?: number; // textarea行数
|
||||
}
|
||||
|
||||
export type UserConfig = { [key: string]: { [key: string]: Config } };
|
||||
|
||||
export interface Script {
|
||||
id: number; // 脚本id
|
||||
uuid: string; // 脚本uuid,通过脚本uuid识别唯一脚本
|
||||
name: string; // 脚本名称
|
||||
code: string; // 脚本执行代码
|
||||
namespace: string; // 脚本命名空间
|
||||
author?: string; // 脚本作者
|
||||
originDomain?: string; // 脚本来源域名
|
||||
origin?: string; // 脚本来源
|
||||
checkUpdateUrl?: string; // 检查更新URL
|
||||
downloadUrl?: string; // 脚本下载URL
|
||||
metadata: Metadata; // 脚本的元数据
|
||||
selfMetadata?: Metadata; // 自定义脚本元数据
|
||||
subscribeUrl?: string; // 如果是通过订阅脚本安装的话,会有订阅地址
|
||||
config?: UserConfig; // 通过UserConfig定义的用户配置
|
||||
type: SCRIPT_TYPE; // 脚本类型 1:普通脚本 2:定时脚本 3:后台脚本
|
||||
status: SCRIPT_STATUS; // 脚本状态 1:启用 2:禁用 3:错误 4:初始化
|
||||
sort: number; // 脚本顺序位置
|
||||
runStatus: SCRIPT_RUN_STATUS; // 脚本运行状态,后台脚本才会有此状态 running:运行中 complete:完成 error:错误 retry:重试
|
||||
error?: { error: string } | string; // 运行错误信息
|
||||
createtime: number; // 脚本创建时间戳
|
||||
updatetime?: number; // 脚本更新时间戳
|
||||
checktime: number; // 脚本检查更新时间戳
|
||||
lastruntime?: number; // 脚本最后一次运行时间戳
|
||||
nextruntime?: number; // 脚本下一次运行时间戳
|
||||
}
|
||||
|
||||
// 脚本运行时的资源,包含已经编译好的脚本与脚本需要的资源
|
||||
export interface ScriptRunResouce extends Script {
|
||||
grantMap: { [key: string]: string };
|
||||
value: { [key: string]: Value };
|
||||
flag: string;
|
||||
resource: { [key: string]: Resource };
|
||||
sourceCode: string;
|
||||
}
|
||||
|
||||
export class ScriptDAO extends DAO<Script> {
|
||||
public tableName = "scripts";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.table = db.table(this.tableName);
|
||||
}
|
||||
|
||||
public findByName(name: string) {
|
||||
return this.findOne({ name });
|
||||
}
|
||||
|
||||
public findByNameAndNamespace(name: string, namespace?: string) {
|
||||
if (namespace) {
|
||||
return this.findOne({ name, namespace });
|
||||
}
|
||||
return this.findOne({ name });
|
||||
}
|
||||
|
||||
public findByUUID(uuid: string) {
|
||||
return this.findOne({ uuid });
|
||||
}
|
||||
|
||||
public findByUUIDAndSubscribeUrl(uuid: string, suburl: string) {
|
||||
return this.findOne({ subscribeUrl: suburl, uuid });
|
||||
}
|
||||
|
||||
public findByOriginAndSubscribeUrl(origin: string, suburl: string) {
|
||||
return this.findOne({ subscribeUrl: suburl, origin });
|
||||
}
|
||||
}
|
39
src/app/repo/subscribe.ts
Normal file
39
src/app/repo/subscribe.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { DAO, db } from "./dao";
|
||||
|
||||
export type Metadata = { [key: string]: string[] };
|
||||
|
||||
export type SUBSCRIBE_STATUS = 1 | 2 | 3 | 4;
|
||||
export const SUBSCRIBE_STATUS_ENABLE: SUBSCRIBE_STATUS = 1;
|
||||
export const SUBSCRIBE_STATUS_DISABLE: SUBSCRIBE_STATUS = 2;
|
||||
|
||||
export interface SubscribeScript {
|
||||
uuid: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface Subscribe {
|
||||
id: number;
|
||||
url: string;
|
||||
name: string;
|
||||
code: string;
|
||||
author: string;
|
||||
scripts: { [key: string]: SubscribeScript };
|
||||
metadata: Metadata;
|
||||
status: SUBSCRIBE_STATUS;
|
||||
createtime: number;
|
||||
updatetime?: number;
|
||||
checktime: number;
|
||||
}
|
||||
|
||||
export class SubscribeDAO extends DAO<Subscribe> {
|
||||
public tableName = "subscribe";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.table = db.table(this.tableName);
|
||||
}
|
||||
|
||||
public findByUrl(url: string) {
|
||||
return this.findOne({ url });
|
||||
}
|
||||
}
|
48
src/app/repo/sync.ts
Normal file
48
src/app/repo/sync.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/* eslint-disable camelcase */
|
||||
export type SyncType = "script" | "subscribe";
|
||||
|
||||
export type SyncAction = "update" | "delete";
|
||||
|
||||
export interface SyncScript {
|
||||
name: string;
|
||||
uuid: string;
|
||||
code: string;
|
||||
meta_json: string;
|
||||
self_meta: string;
|
||||
origin: string;
|
||||
sort: number;
|
||||
subscribe_url?: string;
|
||||
type: number;
|
||||
createtime: number;
|
||||
updatetime?: number;
|
||||
}
|
||||
|
||||
export interface SycnSubscribe {
|
||||
name: string;
|
||||
url: string;
|
||||
code: string;
|
||||
meta_json: string;
|
||||
scripts: string;
|
||||
createtime: number;
|
||||
updatetime?: number;
|
||||
}
|
||||
|
||||
export interface SyncData {
|
||||
action: SyncAction;
|
||||
actiontime: number;
|
||||
uuid?: string;
|
||||
url?: string;
|
||||
msg?: string;
|
||||
script?: SyncScript;
|
||||
subscribe?: SycnSubscribe;
|
||||
}
|
||||
|
||||
export interface Sync {
|
||||
id: number;
|
||||
key: string;
|
||||
user: number;
|
||||
device: number;
|
||||
type: SyncType;
|
||||
data: SyncData;
|
||||
createtime: number;
|
||||
}
|
25
src/app/repo/value.ts
Normal file
25
src/app/repo/value.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import Dexie from "dexie";
|
||||
import { DAO, db } from "./dao";
|
||||
|
||||
export interface Value {
|
||||
id: number;
|
||||
scriptId: number;
|
||||
storageName?: string;
|
||||
key: string;
|
||||
value: any;
|
||||
createtime: number;
|
||||
updatetime: number;
|
||||
}
|
||||
|
||||
export class ValueDAO extends DAO<Value> {
|
||||
public tableName = "value";
|
||||
|
||||
constructor(table?: Dexie.Table<Value, number>) {
|
||||
super();
|
||||
if (table) {
|
||||
this.table = table;
|
||||
} else {
|
||||
this.table = db.table(this.tableName);
|
||||
}
|
||||
}
|
||||
}
|
134
src/app/service/manager/index.ts
Normal file
134
src/app/service/manager/index.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { fetchScriptInfo } from "@App/pkg/utils/script";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Connect } from "@Packages/message";
|
||||
import Cache from "@App/app/cache";
|
||||
import CacheKey from "@App/app/cache_key";
|
||||
import { openInCurrentTab } from "@App/pkg/utils/utils";
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
|
||||
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
|
||||
|
||||
export default class Manager {
|
||||
constructor(private connect: Connect) {}
|
||||
|
||||
listenerScriptInstall() {
|
||||
// 初始化脚本安装监听
|
||||
chrome.webRequest.onBeforeRequest.addListener(
|
||||
(req: chrome.webRequest.WebRequestBodyDetails) => {
|
||||
// 处理url, 实现安装脚本
|
||||
if (req.method !== "GET") {
|
||||
return;
|
||||
}
|
||||
const url = new URL(req.url);
|
||||
// 判断是否有hash
|
||||
if (!url.hash) {
|
||||
return;
|
||||
}
|
||||
// 判断是否有url参数
|
||||
if (!url.hash.includes("url=")) {
|
||||
return;
|
||||
}
|
||||
// 获取url参数
|
||||
const targetUrl = url.hash.split("url=")[1];
|
||||
// 判断是否有bypass参数
|
||||
if (url.hash.includes("bypass=true")) {
|
||||
return;
|
||||
}
|
||||
// 读取脚本url内容, 进行安装
|
||||
LoggerCore.getInstance().logger().debug("install script", { url: targetUrl });
|
||||
this.openInstallPageByUrl(targetUrl).catch(() => {
|
||||
// 如果打开失败, 则重定向到安装页
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId: req.tabId },
|
||||
func: () => {
|
||||
history.back();
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
{
|
||||
urls: [
|
||||
"https://docs.scriptcat.org/docs/script_installation",
|
||||
"https://www.tampermonkey.net/script_installation.php",
|
||||
],
|
||||
types: ["main_frame"],
|
||||
}
|
||||
);
|
||||
// 屏蔽某个tab都安装脚本
|
||||
this.connect.on("install_script", (data: { req: chrome.webRequest.WebRequestBodyDetails }) => {
|
||||
chrome.declarativeNetRequest.updateDynamicRules(
|
||||
{
|
||||
removeRuleIds: [2],
|
||||
addRules: [
|
||||
{
|
||||
id: 2,
|
||||
priority: 1,
|
||||
action: {
|
||||
type: chrome.declarativeNetRequest.RuleActionType.ALLOW,
|
||||
redirect: {
|
||||
regexSubstitution: "https://docs.scriptcat.org/docs/script_installation#url=\\0",
|
||||
},
|
||||
},
|
||||
condition: {
|
||||
regexFilter: "^([^#]+?)\\.user(\\.bg|\\.sub)?\\.js((\\?).*|$)",
|
||||
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
|
||||
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
|
||||
tabIds: [data.req.tabId],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
() => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error(chrome.runtime.lastError);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
// 重定向到脚本安装页
|
||||
chrome.declarativeNetRequest.updateDynamicRules(
|
||||
{
|
||||
removeRuleIds: [1],
|
||||
addRules: [
|
||||
{
|
||||
id: 1,
|
||||
priority: 1,
|
||||
action: {
|
||||
type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,
|
||||
redirect: {
|
||||
regexSubstitution: "https://docs.scriptcat.org/docs/script_installation#url=\\0",
|
||||
},
|
||||
},
|
||||
condition: {
|
||||
regexFilter: "^([^#]+?)\\.user(\\.bg|\\.sub)?\\.js((\\?).*|$)",
|
||||
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
|
||||
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
|
||||
// 排除常见的复合上述条件的域名
|
||||
excludedRequestDomains: ["github.com"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
() => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error(chrome.runtime.lastError);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public openInstallPageByUrl(url: string) {
|
||||
return fetchScriptInfo(url, "user", false, uuidv4()).then((info) => {
|
||||
Cache.getInstance().set(CacheKey.scriptInfo(info.uuid), info);
|
||||
setTimeout(() => {
|
||||
// 清理缓存
|
||||
Cache.getInstance().del(CacheKey.scriptInfo(info.uuid));
|
||||
}, 60 * 1000);
|
||||
openInCurrentTab(`/src/install.html?uuid=${info.uuid}`);
|
||||
});
|
||||
}
|
||||
|
||||
initManager() {
|
||||
this.listenerScriptInstall();
|
||||
}
|
||||
}
|
@ -6,6 +6,6 @@
|
||||
"message": "脚本猫"
|
||||
},
|
||||
"scriptcat_description": {
|
||||
"message": "万物皆可脚本化,让你的浏览器可以做更多的事情!"
|
||||
"message": "万物皆可脚本化,让你的浏览器可以做更多的事情!"
|
||||
}
|
||||
}
|
@ -18,7 +18,13 @@
|
||||
},
|
||||
"default_locale": "zh_CN",
|
||||
"permissions": [
|
||||
"offscreen"
|
||||
"offscreen",
|
||||
"scripting",
|
||||
"webRequest",
|
||||
"declarativeNetRequest"
|
||||
],
|
||||
"host_permissions": [
|
||||
"*://*/*"
|
||||
],
|
||||
"sandbox": {
|
||||
"pages": [
|
||||
|
@ -1,10 +1,22 @@
|
||||
import { Connect, Server } from "@Packages/message";
|
||||
import { extConnect } from "@Packages/message/extension";
|
||||
import { ExtServer } from "@Packages/message/extension";
|
||||
import { WindowServer } from "@Packages/message/window";
|
||||
import migrate from "./app/migrate";
|
||||
import LoggerCore from "./app/logger/core";
|
||||
import DBWriter from "./app/logger/db_writer";
|
||||
import { LoggerDAO } from "./app/repo/logger";
|
||||
|
||||
function main() {
|
||||
// 初始化数据库
|
||||
migrate();
|
||||
// 初始化日志组件
|
||||
const loggerCore = new LoggerCore({
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
writer: new DBWriter(new LoggerDAO()),
|
||||
labels: { env: "offscreen" },
|
||||
});
|
||||
loggerCore.logger().debug("offscreen start");
|
||||
// 与sandbox建立连接
|
||||
const extClient = new Connect(extConnect());
|
||||
const server = new Server(new WindowServer(window));
|
||||
server.on("connection", (con) => {
|
||||
const wrapCon = new Connect(con);
|
||||
@ -12,6 +24,15 @@ function main() {
|
||||
console.log(data);
|
||||
});
|
||||
});
|
||||
// 监听扩展消息
|
||||
const extServer = new Server(new ExtServer());
|
||||
extServer.on("connection", (con) => {
|
||||
const wrapCon = new Connect(con);
|
||||
wrapCon.on("recv", (data, resp) => {
|
||||
console.log(data);
|
||||
resp("service_wwww");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
|
343
src/pkg/utils/script.ts
Normal file
343
src/pkg/utils/script.ts
Normal file
@ -0,0 +1,343 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
Metadata,
|
||||
Script,
|
||||
SCRIPT_RUN_STATUS_COMPLETE,
|
||||
SCRIPT_STATUS_DISABLE,
|
||||
SCRIPT_STATUS_ENABLE,
|
||||
SCRIPT_TYPE_BACKGROUND,
|
||||
SCRIPT_TYPE_CRONTAB,
|
||||
SCRIPT_TYPE_NORMAL,
|
||||
ScriptDAO,
|
||||
UserConfig,
|
||||
} from "@App/app/repo/scripts";
|
||||
import { InstallSource } from "@App/app/service/manager";
|
||||
import YAML from "yaml";
|
||||
import { Subscribe, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe";
|
||||
import { nextTime } from "./utils";
|
||||
|
||||
export function getMetadataStr(code: string): string | null {
|
||||
const start = code.indexOf("==UserScript==");
|
||||
const end = code.indexOf("==/UserScript==");
|
||||
if (start === -1 || end === -1) {
|
||||
return null;
|
||||
}
|
||||
return `// ${code.substring(start, end + 15)}`;
|
||||
}
|
||||
|
||||
export function getUserConfigStr(code: string): string | null {
|
||||
const start = code.indexOf("==UserConfig==");
|
||||
const end = code.indexOf("==/UserConfig==");
|
||||
if (start === -1 || end === -1) {
|
||||
return null;
|
||||
}
|
||||
return `/* ${code.substring(start, end + 15)} */`;
|
||||
}
|
||||
|
||||
export function parseMetadata(code: string): Metadata | null {
|
||||
let issub = false;
|
||||
let regex = /\/\/\s*==UserScript==([\s\S]+?)\/\/\s*==\/UserScript==/m;
|
||||
let header = regex.exec(code);
|
||||
if (!header) {
|
||||
regex = /\/\/\s*==UserSubscribe==([\s\S]+?)\/\/\s*==\/UserSubscribe==/m;
|
||||
header = regex.exec(code);
|
||||
if (!header) {
|
||||
return null;
|
||||
}
|
||||
issub = true;
|
||||
}
|
||||
regex = /\/\/\s*@([\S]+)((.+?)$|$)/gm;
|
||||
const ret: Metadata = {};
|
||||
let meta: RegExpExecArray | null = regex.exec(header[1]);
|
||||
while (meta !== null) {
|
||||
const [key, val] = [meta[1].toLowerCase().trim(), meta[2].trim()];
|
||||
let values = ret[key];
|
||||
if (values == null) {
|
||||
values = [];
|
||||
}
|
||||
values.push(val);
|
||||
ret[key] = values;
|
||||
meta = regex.exec(header[1]);
|
||||
}
|
||||
if (ret.name === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (Object.keys(ret).length < 3) {
|
||||
return null;
|
||||
}
|
||||
if (!ret.namespace) {
|
||||
ret.namespace = [""];
|
||||
}
|
||||
if (issub) {
|
||||
ret.usersubscribe = [];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function parseUserConfig(code: string): UserConfig | undefined {
|
||||
const regex = /\/\*\s*==UserConfig==([\s\S]+?)\s*==\/UserConfig==\s*\*\//m;
|
||||
const config = regex.exec(code);
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
const configs = config[1].trim().split(/[-]{3,}/);
|
||||
const ret: UserConfig = {};
|
||||
configs.forEach((val) => {
|
||||
const obj: UserConfig = YAML.parse(val);
|
||||
Object.keys(obj).forEach((key) => {
|
||||
ret[key] = obj[key];
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
export type ScriptInfo = {
|
||||
url: string;
|
||||
code: string;
|
||||
uuid: string;
|
||||
isSubscribe: boolean;
|
||||
isUpdate: boolean;
|
||||
metadata: Metadata;
|
||||
source: InstallSource;
|
||||
};
|
||||
|
||||
export async function fetchScriptInfo(
|
||||
url: string,
|
||||
source: InstallSource,
|
||||
isUpdate: boolean,
|
||||
uuid: string
|
||||
): Promise<ScriptInfo> {
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
});
|
||||
if (resp.status !== 200) {
|
||||
throw new Error("fetch script info failed");
|
||||
}
|
||||
if (resp.headers.get("content-type")?.indexOf("text/html") !== -1) {
|
||||
throw new Error("url is html");
|
||||
}
|
||||
|
||||
const body = await resp.text();
|
||||
const parse = parseMetadata(body);
|
||||
if (!parse) {
|
||||
throw new Error("parse script info failed");
|
||||
}
|
||||
const ret: ScriptInfo = {
|
||||
url,
|
||||
code: body,
|
||||
uuid,
|
||||
isSubscribe: false,
|
||||
isUpdate,
|
||||
metadata: parse,
|
||||
source,
|
||||
};
|
||||
if (parse.usersubscribe) {
|
||||
ret.isSubscribe = true;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function copyScript(script: Script, old: Script): Script {
|
||||
const ret = script;
|
||||
ret.id = old.id;
|
||||
ret.uuid = old.uuid;
|
||||
ret.createtime = old.createtime;
|
||||
ret.lastruntime = old.lastruntime;
|
||||
// ret.delayruntime = old.delayruntime;
|
||||
ret.error = old.error;
|
||||
ret.sort = old.sort;
|
||||
if (!ret.selfMetadata) {
|
||||
ret.selfMetadata = old.selfMetadata || {};
|
||||
}
|
||||
ret.subscribeUrl = old.subscribeUrl;
|
||||
ret.status = old.status;
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function copySubscribe(sub: Subscribe, old: Subscribe): Subscribe {
|
||||
const ret = sub;
|
||||
ret.id = old.id;
|
||||
ret.scripts = old.scripts;
|
||||
ret.createtime = old.createtime;
|
||||
ret.status = old.status;
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(<string>reader.result);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
export function blobToText(blob: Blob): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(<string | null>reader.result);
|
||||
reader.readAsText(blob);
|
||||
});
|
||||
}
|
||||
|
||||
export function base64ToBlob(dataURI: string) {
|
||||
const mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
|
||||
const byteString = atob(dataURI.split(",")[1]);
|
||||
const arrayBuffer = new ArrayBuffer(byteString.length);
|
||||
const intArray = new Uint8Array(arrayBuffer);
|
||||
|
||||
for (let i = 0; i < byteString.length; i += 1) {
|
||||
intArray[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
return new Blob([intArray], { type: mimeString });
|
||||
}
|
||||
|
||||
export function strToBase64(str: string): string {
|
||||
return btoa(
|
||||
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1: string) => {
|
||||
return String.fromCharCode(parseInt(`0x${p1}`, 16));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 通过代码解析出脚本信息
|
||||
export function prepareScriptByCode(
|
||||
code: string,
|
||||
url: string,
|
||||
uuid?: string,
|
||||
override?: boolean
|
||||
): Promise<{ script: Script; oldScript?: Script }> {
|
||||
const dao = new ScriptDAO();
|
||||
return new Promise((resolve, reject) => {
|
||||
const metadata = parseMetadata(code);
|
||||
if (metadata == null) {
|
||||
throw new Error("MetaData信息错误");
|
||||
}
|
||||
if (metadata.name === undefined) {
|
||||
throw new Error("脚本名不能为空");
|
||||
}
|
||||
if (metadata.version === undefined) {
|
||||
throw new Error("脚本@version版本不能为空");
|
||||
}
|
||||
if (metadata.namespace === undefined) {
|
||||
throw new Error("脚本@namespace命名空间不能为空");
|
||||
}
|
||||
let type = SCRIPT_TYPE_NORMAL;
|
||||
if (metadata.crontab !== undefined) {
|
||||
type = SCRIPT_TYPE_CRONTAB;
|
||||
try {
|
||||
nextTime(metadata.crontab[0]);
|
||||
} catch {
|
||||
throw new Error(`错误的定时表达式,请检查: ${metadata.crontab[0]}`);
|
||||
}
|
||||
} else if (metadata.background !== undefined) {
|
||||
type = SCRIPT_TYPE_BACKGROUND;
|
||||
}
|
||||
let urlSplit: string[];
|
||||
let domain = "";
|
||||
let checkUpdateUrl = "";
|
||||
let downloadUrl = url;
|
||||
if (metadata.updateurl && metadata.downloadurl) {
|
||||
[checkUpdateUrl] = metadata.updateurl;
|
||||
[downloadUrl] = metadata.downloadurl;
|
||||
} else {
|
||||
checkUpdateUrl = url.replace("user.js", "meta.js");
|
||||
}
|
||||
if (url.indexOf("/") !== -1) {
|
||||
urlSplit = url.split("/");
|
||||
if (urlSplit[2]) {
|
||||
[, domain] = urlSplit;
|
||||
}
|
||||
}
|
||||
let newUUID = "";
|
||||
if (uuid) {
|
||||
newUUID = uuid;
|
||||
} else {
|
||||
newUUID = uuidv4();
|
||||
}
|
||||
let script: Script = {
|
||||
id: 0,
|
||||
uuid: newUUID,
|
||||
name: metadata.name[0],
|
||||
code,
|
||||
author: metadata.author && metadata.author[0],
|
||||
namespace: metadata.namespace && metadata.namespace[0],
|
||||
originDomain: domain,
|
||||
origin: url,
|
||||
checkUpdateUrl,
|
||||
downloadUrl,
|
||||
config: parseUserConfig(code),
|
||||
metadata,
|
||||
selfMetadata: {},
|
||||
sort: -1,
|
||||
type,
|
||||
status: SCRIPT_STATUS_DISABLE,
|
||||
runStatus: SCRIPT_RUN_STATUS_COMPLETE,
|
||||
createtime: Date.now(),
|
||||
updatetime: Date.now(),
|
||||
checktime: Date.now(),
|
||||
};
|
||||
const handler = async () => {
|
||||
let old: Script | undefined;
|
||||
if (uuid) {
|
||||
old = await dao.findByUUID(uuid);
|
||||
if (!old && override) {
|
||||
old = await dao.findByNameAndNamespace(script.name, script.namespace);
|
||||
}
|
||||
} else {
|
||||
old = await dao.findByNameAndNamespace(script.name, script.namespace);
|
||||
}
|
||||
if (old) {
|
||||
if (
|
||||
(old.type === SCRIPT_TYPE_NORMAL && script.type !== SCRIPT_TYPE_NORMAL) ||
|
||||
(script.type === SCRIPT_TYPE_NORMAL && old.type !== SCRIPT_TYPE_NORMAL)
|
||||
) {
|
||||
reject(new Error("脚本类型不匹配,普通脚本与后台脚本不能互相转变"));
|
||||
return;
|
||||
}
|
||||
script = copyScript(script, old);
|
||||
} else {
|
||||
// 前台脚本默认开启
|
||||
if (script.type === SCRIPT_TYPE_NORMAL) {
|
||||
script.status = SCRIPT_STATUS_ENABLE;
|
||||
}
|
||||
script.checktime = new Date().getTime();
|
||||
}
|
||||
resolve({ script, oldScript: old });
|
||||
};
|
||||
handler();
|
||||
});
|
||||
}
|
||||
|
||||
export async function prepareSubscribeByCode(
|
||||
code: string,
|
||||
url: string
|
||||
): Promise<{ subscribe: Subscribe; oldSubscribe?: Subscribe }> {
|
||||
const dao = new SubscribeDAO();
|
||||
const metadata = parseMetadata(code);
|
||||
if (metadata == null) {
|
||||
throw new Error("MetaData信息错误");
|
||||
}
|
||||
if (metadata.name === undefined) {
|
||||
throw new Error("订阅名不能为空");
|
||||
}
|
||||
let subscribe: Subscribe = {
|
||||
id: 0,
|
||||
url,
|
||||
name: metadata.name[0],
|
||||
code,
|
||||
author: metadata.author && metadata.author[0],
|
||||
scripts: {},
|
||||
metadata,
|
||||
status: SUBSCRIBE_STATUS_ENABLE,
|
||||
createtime: Date.now(),
|
||||
updatetime: Date.now(),
|
||||
checktime: Date.now(),
|
||||
};
|
||||
const old = await dao.findByUrl(url);
|
||||
if (old) {
|
||||
subscribe = copySubscribe(subscribe, old);
|
||||
}
|
||||
return Promise.resolve({ subscribe, oldSubscribe: old });
|
||||
}
|
96
src/pkg/utils/utils.test.ts
Normal file
96
src/pkg/utils/utils.test.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { formatTime, nextTime, ltever, checkSilenceUpdate } from "./utils";
|
||||
import dayjs from "dayjs";
|
||||
describe("nextTime", () => {
|
||||
test("每分钟表达式", () => {
|
||||
expect(nextTime("* * * * *")).toEqual(
|
||||
dayjs(new Date()).add(1, "minute").format("YYYY-MM-DD HH:mm:00")
|
||||
);
|
||||
});
|
||||
test("每分钟一次表达式", () => {
|
||||
expect(nextTime("once * * * *")).toEqual(
|
||||
dayjs(new Date())
|
||||
.add(1, "minute")
|
||||
.format("YYYY-MM-DD HH:mm 每分钟运行一次")
|
||||
);
|
||||
});
|
||||
test("每小时一次表达式", () => {
|
||||
expect(nextTime("* once * * *")).toEqual(
|
||||
dayjs(new Date()).add(1, "hour").format("YYYY-MM-DD HH 每小时运行一次")
|
||||
);
|
||||
});
|
||||
test("每天一次表达式", () => {
|
||||
expect(nextTime("* * once * *")).toEqual(
|
||||
dayjs(new Date()).add(1, "day").format("YYYY-MM-DD 每天运行一次")
|
||||
);
|
||||
});
|
||||
test("每月一次表达式", () => {
|
||||
expect(nextTime("* * * once *")).toEqual(
|
||||
dayjs(new Date()).add(1, "month").format("YYYY-MM 每月运行一次")
|
||||
);
|
||||
});
|
||||
test("每星期一次表达式", () => {
|
||||
expect(nextTime("* * * * once")).toEqual(
|
||||
dayjs(new Date()).add(1, "week").format("YYYY-MM-DD 每星期运行一次")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ltever", () => {
|
||||
it("semver", () => {
|
||||
expect(ltever("1.0.0", "1.0.1")).toBe(true);
|
||||
expect(ltever("1.0.0", "1.0.0")).toBe(true);
|
||||
expect(ltever("1.0.1", "1.0.0")).toBe(false);
|
||||
});
|
||||
it("any", () => {
|
||||
expect(ltever("1.2.3.4", "1.2.3.4")).toBe(true);
|
||||
expect(ltever("1.2.3.4", "1.2.3.5")).toBe(true);
|
||||
expect(ltever("1.2.3.4", "1.2.3.3")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkSilenceUpdate", () => {
|
||||
it("true", () => {
|
||||
expect(
|
||||
checkSilenceUpdate(
|
||||
{
|
||||
connect: ["www.baidu.com"],
|
||||
},
|
||||
{
|
||||
connect: ["www.baidu.com"],
|
||||
}
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
checkSilenceUpdate(
|
||||
{
|
||||
connect: ["www.baidu.com", "scriptcat.org"],
|
||||
},
|
||||
{
|
||||
connect: ["scriptcat.org"],
|
||||
}
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
it("false", () => {
|
||||
expect(
|
||||
checkSilenceUpdate(
|
||||
{
|
||||
connect: ["www.baidu.com"],
|
||||
},
|
||||
{
|
||||
connect: ["www.google.com"],
|
||||
}
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
checkSilenceUpdate(
|
||||
{
|
||||
connect: ["www.baidu.com"],
|
||||
},
|
||||
{
|
||||
connect: ["www.baidu.com", "scriptcat.org"],
|
||||
}
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
184
src/pkg/utils/utils.ts
Normal file
184
src/pkg/utils/utils.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { CronTime } from "cron";
|
||||
import dayjs from "dayjs";
|
||||
import semver from "semver";
|
||||
|
||||
export function nextTime(crontab: string): string {
|
||||
let oncePos = 0;
|
||||
if (crontab.indexOf("once") !== -1) {
|
||||
const vals = crontab.split(" ");
|
||||
vals.forEach((val, index) => {
|
||||
if (val === "once") {
|
||||
oncePos = index;
|
||||
}
|
||||
});
|
||||
if (vals.length === 5) {
|
||||
oncePos += 1;
|
||||
}
|
||||
}
|
||||
let cron: CronTime;
|
||||
try {
|
||||
cron = new CronTime(crontab.replace(/once/g, "*"));
|
||||
} catch {
|
||||
throw new Error("错误的定时表达式");
|
||||
}
|
||||
if (oncePos) {
|
||||
switch (oncePos) {
|
||||
case 1: // 每分钟
|
||||
return cron.sendAt().toFormat("yyyy-MM-dd HH:mm 每分钟运行一次");
|
||||
case 2: // 每小时
|
||||
return cron.sendAt().plus({ hour: 1 }).toFormat("yyyy-MM-dd HH 每小时运行一次");
|
||||
case 3: // 每天
|
||||
return cron.sendAt().plus({ day: 1 }).toFormat("yyyy-MM-dd 每天运行一次");
|
||||
case 4: // 每月
|
||||
return cron.sendAt().plus({ month: 1 }).toFormat("yyyy-MM 每月运行一次");
|
||||
case 5: // 每星期
|
||||
return cron.sendAt().plus({ week: 1 }).toFormat("yyyy-MM-dd 每星期运行一次");
|
||||
}
|
||||
throw new Error("错误表达式");
|
||||
}
|
||||
return cron.sendAt().toFormat("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
export function formatTime(time: Date) {
|
||||
return dayjs(time).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
||||
export function formatUnixTime(time: number) {
|
||||
return dayjs.unix(time).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
||||
export function semTime(time: Date) {
|
||||
return dayjs().to(dayjs(time));
|
||||
}
|
||||
|
||||
export function randomString(e: number) {
|
||||
e = e || 32;
|
||||
const t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz";
|
||||
const a = t.length;
|
||||
let n = "";
|
||||
for (let i = 0; i < e; i += 1) {
|
||||
n += t.charAt(Math.floor(Math.random() * a));
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
export function dealSymbol(source: string): string {
|
||||
source = source.replace(/("|\\)/g, "\\$1");
|
||||
source = source.replace(/(\r\n|\n)/g, "\\n");
|
||||
return source;
|
||||
}
|
||||
|
||||
export function dealScript(source: string): string {
|
||||
return dealSymbol(source);
|
||||
}
|
||||
|
||||
export function isFirefox() {
|
||||
if (navigator.userAgent.indexOf("Firefox") >= 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function InfoNotification(title: string, msg: string) {
|
||||
chrome.notifications.create({
|
||||
type: "basic",
|
||||
title,
|
||||
message: msg,
|
||||
iconUrl: chrome.runtime.getURL("assets/logo.png"),
|
||||
});
|
||||
}
|
||||
|
||||
export function valueType(val: unknown) {
|
||||
switch (typeof val) {
|
||||
case "string":
|
||||
case "number":
|
||||
case "boolean":
|
||||
case "object":
|
||||
return typeof val;
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export function toStorageValueStr(val: unknown): string {
|
||||
switch (typeof val) {
|
||||
case "string":
|
||||
return `s${val}`;
|
||||
case "number":
|
||||
return `n${val.toString()}`;
|
||||
case "boolean":
|
||||
return `b${val ? "true" : "false"}`;
|
||||
default:
|
||||
try {
|
||||
return `o${JSON.stringify(val)}`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseStorageValue(str: string): unknown {
|
||||
if (str === "") {
|
||||
return undefined;
|
||||
}
|
||||
const t = str[0];
|
||||
const s = str.substring(1);
|
||||
switch (t) {
|
||||
case "b":
|
||||
return s === "true";
|
||||
case "n":
|
||||
return parseFloat(s);
|
||||
case "o":
|
||||
try {
|
||||
return JSON.parse(s);
|
||||
} catch {
|
||||
return str;
|
||||
}
|
||||
case "s":
|
||||
return s;
|
||||
default:
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
// 对比版本大小
|
||||
export function ltever(newVersion: string, oldVersion: string) {
|
||||
// 先验证符不符合语义化版本规范
|
||||
try {
|
||||
return semver.lte(newVersion, oldVersion);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
const newVer = newVersion.split(".");
|
||||
const oldVer = oldVersion.split(".");
|
||||
for (let i = 0; i < newVer.length; i += 1) {
|
||||
if (Number(newVer[i]) > Number(oldVer[i])) {
|
||||
return false;
|
||||
}
|
||||
if (Number(newVer[i]) < Number(oldVer[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 在当前页后打开一个新页面
|
||||
export function openInCurrentTab(url: string) {
|
||||
chrome.tabs.query(
|
||||
{
|
||||
active: true,
|
||||
},
|
||||
(tabs) => {
|
||||
if (tabs.length) {
|
||||
chrome.tabs.create({
|
||||
url,
|
||||
index: tabs[0].index + 1,
|
||||
});
|
||||
} else {
|
||||
chrome.tabs.create({
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
@ -1,34 +1,65 @@
|
||||
import { ExtServer } from "@Packages/message/extension";
|
||||
import { Connect, Server } from "@Packages/message";
|
||||
import { extConnect } from "@Packages/message/extension";
|
||||
import Manager from "./app/service/manager";
|
||||
import { Connect } from "@Packages/message";
|
||||
import migrate from "./app/migrate";
|
||||
import LoggerCore from "./app/logger/core";
|
||||
import DBWriter from "./app/logger/db_writer";
|
||||
import { LoggerDAO } from "./app/repo/logger";
|
||||
|
||||
const OFFSCREEN_DOCUMENT_PATH = "src/offscreen.html";
|
||||
|
||||
let creating: Promise<void> | null;
|
||||
|
||||
async function hasDocument() {
|
||||
const offscreenUrl = chrome.runtime.getURL(OFFSCREEN_DOCUMENT_PATH);
|
||||
const existingContexts = await chrome.runtime.getContexts({
|
||||
contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT],
|
||||
documentUrls: [offscreenUrl],
|
||||
});
|
||||
|
||||
return existingContexts.length > 0;
|
||||
}
|
||||
|
||||
async function setupOffscreenDocument() {
|
||||
// 创建运行后台脚本的沙盒环境
|
||||
await chrome.offscreen.createDocument({
|
||||
url: "src/offscreen.html",
|
||||
reasons: [chrome.offscreen.Reason.CLIPBOARD],
|
||||
justification: "offscreen page",
|
||||
});
|
||||
//if we do not have a document, we are already setup and can skip
|
||||
if (!(await hasDocument())) {
|
||||
// create offscreen document
|
||||
if (creating) {
|
||||
await creating;
|
||||
} else {
|
||||
creating = chrome.offscreen.createDocument({
|
||||
url: OFFSCREEN_DOCUMENT_PATH,
|
||||
reasons: [
|
||||
chrome.offscreen.Reason.CLIPBOARD,
|
||||
chrome.offscreen.Reason.DOM_SCRAPING,
|
||||
chrome.offscreen.Reason.LOCAL_STORAGE,
|
||||
],
|
||||
justification: "offscreen page",
|
||||
});
|
||||
|
||||
// Send message to offscreen document
|
||||
chrome.runtime.sendMessage({
|
||||
type: "init",
|
||||
target: "offscreen",
|
||||
data: { init: "1" },
|
||||
});
|
||||
await creating;
|
||||
creating = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 监听消息
|
||||
const server = new Server(new ExtServer());
|
||||
server.on("connection", (con) => {
|
||||
const wrapCon = new Connect(con);
|
||||
wrapCon.on("recv", (data,resp) => {
|
||||
console.log(data);
|
||||
resp("service_wwww");
|
||||
});
|
||||
// 初始化数据库
|
||||
migrate();
|
||||
// 初始化日志组件
|
||||
const loggerCore = new LoggerCore({
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
writer: new DBWriter(new LoggerDAO()),
|
||||
labels: { env: "background" },
|
||||
});
|
||||
loggerCore.logger().debug("background start");
|
||||
// 初始化沙盒环境
|
||||
await setupOffscreenDocument();
|
||||
// 初始化连接
|
||||
const extClient = new Connect(extConnect());
|
||||
// 初始化管理器
|
||||
const manager = new Manager(extClient);
|
||||
manager.initManager();
|
||||
}
|
||||
|
||||
main();
|
||||
|
Reference in New Issue
Block a user