diff --git a/package.json b/package.json index 5b6cd2a..a67f2d9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b77ad3e..e9cd90f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/rspack.config.ts b/rspack.config.ts index 091d211..a59fe0b 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -39,6 +39,9 @@ export default defineConfig({ "@App": path.resolve(__dirname, "src/"), "@Packages": path.resolve(__dirname, "packages/"), }, + fallback: { + child_process: false, + }, }, module: { rules: [ diff --git a/src/app/cache.ts b/src/app/cache.ts new file mode 100644 index 0000000..ec771e9 --- /dev/null +++ b/src/app/cache.ts @@ -0,0 +1,42 @@ +export default class Cache { + static instance: Cache = new Cache(); + + static getInstance(): Cache { + return Cache.instance; + } + + map: Map; + + private constructor() { + this.map = new Map(); + } + + public get(key: string): unknown { + return this.map.get(key); + } + + public async getOrSet(key: string, set: () => Promise): Promise { + 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()); + } +} diff --git a/src/app/cache_key.ts b/src/app/cache_key.ts new file mode 100644 index 0000000..844eba1 --- /dev/null +++ b/src/app/cache_key.ts @@ -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}`; + } +} diff --git a/src/app/logger/core.ts b/src/app/logger/core.ts new file mode 100644 index 0000000..6b8f5ed --- /dev/null +++ b/src/app/logger/core.ts @@ -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(); +} diff --git a/src/app/logger/db_writer.ts b/src/app/logger/db_writer.ts new file mode 100644 index 0000000..7be008c --- /dev/null +++ b/src/app/logger/db_writer.ts @@ -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(), + }); + } +} diff --git a/src/app/logger/logger.ts b/src/app/logger/logger.ts new file mode 100644 index 0000000..0430d4d --- /dev/null +++ b/src/app/logger/logger.ts @@ -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 {}; + } +} diff --git a/src/app/logger/message_writer.ts b/src/app/logger/message_writer.ts new file mode 100644 index 0000000..00c28d6 --- /dev/null +++ b/src/app/logger/message_writer.ts @@ -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); + }); +} diff --git a/src/app/migrate.ts b/src/app/migrate.ts new file mode 100644 index 0000000..e72569c --- /dev/null +++ b/src/app/migrate.ts @@ -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(); +} diff --git a/src/app/repo/dao.test.ts b/src/app/repo/dao.test.ts new file mode 100644 index 0000000..935d4d9 --- /dev/null +++ b/src/app/repo/dao.test.ts @@ -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 { + 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); + }); +}); diff --git a/src/app/repo/dao.ts b/src/app/repo/dao.ts new file mode 100644 index 0000000..eb1351c --- /dev/null +++ b/src/app/repo/dao.ts @@ -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 { + public table!: Dexie.Table; + + 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 = (val).id; + if (!id) { + delete (val).id; + return this.table.add(val); + } + const resp = await this.table.update(id, 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(); + } +} diff --git a/src/app/repo/export.ts b/src/app/repo/export.ts new file mode 100644 index 0000000..216ed28 --- /dev/null +++ b/src/app/repo/export.ts @@ -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 { + public tableName = "export"; + + constructor() { + super(); + this.table = db.table(this.tableName); + } + + findByScriptID(scriptID: number) { + return this.table.where({ scriptId: scriptID }).first(); + } +} diff --git a/src/app/repo/logger.ts b/src/app/repo/logger.ts new file mode 100644 index 0000000..d18f329 --- /dev/null +++ b/src/app/repo/logger.ts @@ -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 { + 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(); + } +} diff --git a/src/app/repo/permission.ts b/src/app/repo/permission.ts new file mode 100644 index 0000000..39edb21 --- /dev/null +++ b/src/app/repo/permission.ts @@ -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 { + public tableName = "permission"; + + constructor() { + super(); + this.table = db.table(this.tableName); + } +} diff --git a/src/app/repo/resource.ts b/src/app/repo/resource.ts new file mode 100644 index 0000000..795d13e --- /dev/null +++ b/src/app/repo/resource.ts @@ -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 { + public tableName = "resource"; + + constructor() { + super(); + this.table = db.table(this.tableName); + } +} diff --git a/src/app/repo/resource_link.ts b/src/app/repo/resource_link.ts new file mode 100644 index 0000000..3425a96 --- /dev/null +++ b/src/app/repo/resource_link.ts @@ -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 { + public tableName = "resourceLink"; + + constructor() { + super(); + this.table = db.table(this.tableName); + } +} diff --git a/src/app/repo/scripts.ts b/src/app/repo/scripts.ts new file mode 100644 index 0000000..5776870 --- /dev/null +++ b/src/app/repo/scripts.ts @@ -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