diff --git a/apps/core/src/app.module.ts b/apps/core/src/app.module.ts index e4164e8f8..557ac8981 100644 --- a/apps/core/src/app.module.ts +++ b/apps/core/src/app.module.ts @@ -24,6 +24,7 @@ import { ConfigPublicModule } from './modules/configs/configs.module'; import { REDIS_TRANSPORTER } from '~/shared/constants/transporter.constants'; import { ConsoleModule } from './modules/console/console.module'; import { ThemesModule } from './modules/themes/themes.module'; +import { StoreModule } from './modules/store/store.module'; @Module({ imports: [ @@ -41,6 +42,7 @@ import { ThemesModule } from './modules/themes/themes.module'; ConfigPublicModule, ConsoleModule, ThemesModule, + StoreModule, ClientsModule.register([ { name: ServicesEnum.notification, diff --git a/apps/core/src/common/adapt/fastify.adapt.ts b/apps/core/src/common/adapt/fastify.adapt.ts index 0a3085c2f..0ef1e4acc 100644 --- a/apps/core/src/common/adapt/fastify.adapt.ts +++ b/apps/core/src/common/adapt/fastify.adapt.ts @@ -14,8 +14,6 @@ const app: FastifyAdapter = new FastifyAdapter({ }); export { app as fastifyApp }; -// @ts-ignore -// HACK: There is nothing wrong during runtime, but the type is wrong. I don't know why. app.register(FastifyMultipart, { limits: { fields: 10, // Max number of non-file fields diff --git a/apps/core/src/modules/store/store.controller.ts b/apps/core/src/modules/store/store.controller.ts new file mode 100644 index 000000000..e9cfee5a6 --- /dev/null +++ b/apps/core/src/modules/store/store.controller.ts @@ -0,0 +1,127 @@ +import { + Body, + Controller, + Get, + Inject, + Param, + Post, + Req, + Res, +} from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { ApiOperation } from '@nestjs/swagger'; +import { FastifyReply, FastifyRequest } from 'fastify'; +import { Auth } from '~/shared/common/decorator/auth.decorator'; +import { HTTPDecorators } from '~/shared/common/decorator/http.decorator'; +import { ApiName } from '~/shared/common/decorator/openapi.decorator'; +import { StoreEvents } from '~/shared/constants/event.constant'; +import { ServicesEnum } from '~/shared/constants/services.constant'; +import { BadRequestRpcExcption } from '~/shared/exceptions/bad-request-rpc-exception'; +import { transportReqToMicroservice } from '~/shared/microservice.transporter'; + +@Controller('store') +@ApiName +export class StoreController { + constructor( + @Inject(ServicesEnum.store) private readonly store: ClientProxy, + ) {} + + @Get('/ping') + @ApiOperation({ summary: '检测服务是否在线' }) + ping() { + return transportReqToMicroservice(this.store, StoreEvents.Ping, {}); + } + + @Get(['/list/*', '/list']) + @Auth() + @ApiOperation({ summary: '获取文件列表' }) + list(@Param('*') path?: string) { + return transportReqToMicroservice( + this.store, + StoreEvents.StoreFileGetList, + path || '', + ); + } + + @Get('/raw/*') + @ApiOperation({ summary: '获取文件' }) + async raw(@Param('*') path: string, @Res() res: FastifyReply) { + const data = await transportReqToMicroservice<{ + file: Buffer; + name: string; + ext: string; + mimetype: string; + }>(this.store, StoreEvents.StoreFileGet, path); + if (data.mimetype) { + res.type(data.mimetype); + res.header('cache-control', 'public, max-age=31536000'); + res.header( + 'expires', + new Date(Date.now() + 31536000 * 1000).toUTCString(), + ); + } + const buffer = Buffer.from(data.file); + res.send(buffer); + } + + @Post('/download') + @ApiOperation({ summary: '从远端下载文件' }) + @Auth() + download(@Body('url') url: string, @Body('path') path?: string) { + return transportReqToMicroservice( + this.store, + StoreEvents.StoreFileDownloadFromRemote, + { url, path }, + ); + } + + @Post('/upload') + @ApiOperation({ summary: '上传文件' }) + // @Auth() + @HTTPDecorators.FileUpload({ description: 'upload file' }) + async upload(@Req() req: FastifyRequest, @Body('path') _path?: string) { + const data = await req.file(); + + if (!data) { + throw new BadRequestRpcExcption('仅能上传文件!'); + } + if (data.fieldname != 'file') { + throw new BadRequestRpcExcption('字段必须为 file'); + } + + const filename = data.filename; + return transportReqToMicroservice( + this.store, + StoreEvents.StoreFileUploadByMaster, + { + file: { + filename, + file: await data.toBuffer(), + }, + path: _path, + }, + ); + } + + @Post('/delete') + @ApiOperation({ summary: '删除文件' }) + @Auth() + delete(@Body('path') path: string) { + return transportReqToMicroservice( + this.store, + StoreEvents.StoreFileDeleteByMaster, + { path }, + ); + } + + @Post('/mkdir') + @ApiOperation({ summary: '创建文件夹' }) + @Auth() + mkdir(@Body('path') path: string) { + return transportReqToMicroservice( + this.store, + StoreEvents.StoreFileMkdirByMaster, + { path }, + ); + } +} diff --git a/apps/core/src/modules/store/store.module.ts b/apps/core/src/modules/store/store.module.ts new file mode 100644 index 000000000..1bdadcf9a --- /dev/null +++ b/apps/core/src/modules/store/store.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { ClientsModule } from '@nestjs/microservices'; +import { ServicesEnum } from '~/shared/constants/services.constant'; +import { REDIS_TRANSPORTER } from '~/shared/constants/transporter.constants'; +import { StoreController } from './store.controller'; + +@Module({ + imports: [ + ClientsModule.register([ + { + name: ServicesEnum.store, + ...REDIS_TRANSPORTER, + }, + ]), + ], + controllers: [StoreController], +}) +export class StoreModule {} diff --git a/apps/store-service/src/main.ts b/apps/store-service/src/main.ts new file mode 100644 index 000000000..34e49ee03 --- /dev/null +++ b/apps/store-service/src/main.ts @@ -0,0 +1,35 @@ +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { REDIS } from '~/apps/core/src/app.config'; +import { BasicCommer } from '~/shared/commander'; +import { registerStdLogger } from '~/shared/global/consola.global'; +import { mkStoreDir, registerGlobal } from '~/shared/global/index.global'; +import { readEnv } from '~/shared/utils/rag-env'; +import { StoreServiceModule } from './store-service.module'; + +async function bootstrap() { + registerGlobal(); + registerStdLogger("store"); + mkStoreDir(); + + const argv = BasicCommer.parse().opts(); + readEnv(argv, argv.config); + + const app = await NestFactory.createMicroservice( + StoreServiceModule, + { + transport: Transport.REDIS, + options: { + port: REDIS.port, + host: REDIS.host, + password: REDIS.password, + username: REDIS.user, + }, + }, + ); + app.listen().then(() => { + Logger.log(`>> StoreService 正在工作... <<`) + }) +} +bootstrap(); diff --git a/apps/store-service/src/store-service.controller.ts b/apps/store-service/src/store-service.controller.ts new file mode 100644 index 000000000..677182a45 --- /dev/null +++ b/apps/store-service/src/store-service.controller.ts @@ -0,0 +1,52 @@ +import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { StoreEvents } from '~/shared/constants/event.constant'; +import { StoreServiceService } from './store-service.service'; + +@Controller() +export class StoreServiceController { + constructor(private readonly storeServiceService: StoreServiceService) {} + + @MessagePattern({ cmd: StoreEvents.Ping }) + ping() { + return true; + } + + @MessagePattern({ cmd: StoreEvents.StoreFileUploadByMaster }) + async storeFileUpload(data: { + file: { + filename: string; + file: Buffer; + }; + path?: string; + }) { + data.file.file = Buffer.from(data.file.file); + return await this.storeServiceService.storeFile(data.file, data.path); + } + + @MessagePattern({ cmd: StoreEvents.StoreFileDownloadFromRemote }) + async storeFileDownloadFromRemote(data: { url: string; path?: string }) { + return await this.storeServiceService.downloadFile(data.url, data.path); + } + + @MessagePattern({ cmd: StoreEvents.StoreFileDeleteByMaster }) + async storeFileDelete(path: string) { + return await this.storeServiceService.deleteFile(path); + } + + @MessagePattern({ cmd: StoreEvents.StoreFileGet }) + async storeFileGet(path: string) { + return await this.storeServiceService.getFile(path); + } + + @MessagePattern({ cmd: StoreEvents.StoreFileGetList }) + async storeFileList(path?: string) { + return await this.storeServiceService.getFileList(path); + } + + @MessagePattern({ cmd: StoreEvents.StoreFileMkdirByMaster }) + async storeFileMkdir(path: string) { + return await this.storeServiceService.mkdir(path); + } + +} diff --git a/apps/store-service/src/store-service.module.ts b/apps/store-service/src/store-service.module.ts new file mode 100644 index 000000000..e751c1eb2 --- /dev/null +++ b/apps/store-service/src/store-service.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { HelperModule } from '~/libs/helper/src'; +import { StoreServiceController } from './store-service.controller'; +import { StoreServiceService } from './store-service.service'; + +@Module({ + imports: [HelperModule], + controllers: [StoreServiceController], + providers: [StoreServiceService], +}) +export class StoreServiceModule {} diff --git a/apps/store-service/src/store-service.service.ts b/apps/store-service/src/store-service.service.ts new file mode 100644 index 000000000..be1d32983 --- /dev/null +++ b/apps/store-service/src/store-service.service.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@nestjs/common'; +import { join } from 'path'; +import { AssetsService } from '~/libs/helper/src/helper.assets.service'; +import { STORE_DIR } from '~/shared/constants/path.constant'; +import { InternalServerErrorRpcExcption } from '~/shared/exceptions/internal-server-error-rpc-exception'; + +@Injectable() +export class StoreServiceService { + constructor(private readonly assetHelper: AssetsService) {} + + async storeFile( + data: { + filename: string; + file: Buffer; + }, + path?: string, + ) { + const _path = join(STORE_DIR, path || ''); + const name = data.filename; + if (this.assetHelper.exists(join(_path, name))) { + throw new InternalServerErrorRpcExcption('文件夹已存在'); + } + return await this.assetHelper + .writeFile(data.file, _path, name) + .catch((e) => { + console.log(e); + throw new InternalServerErrorRpcExcption(e); + }); + } + + async downloadFile(url: string, path?: string) { + const _path = join(STORE_DIR, path || ''); + await this.assetHelper.downloadFile(url, _path).catch((e) => { + throw new InternalServerErrorRpcExcption(e); + }); + return true; + } + + async deleteFile(path: string) { + await this.assetHelper.deleteFile(path).catch((e) => { + throw new InternalServerErrorRpcExcption(e); + }); + return true; + } + + async getFile(path: string) { + const _path = join(STORE_DIR, path || ''); + return await this.assetHelper.getFile(_path).catch((e) => { + throw new InternalServerErrorRpcExcption(e); + }); + } + + async getFileList(path?: string) { + const _path = join(STORE_DIR, path || ''); + return await this.assetHelper.getFileList(_path).catch((e) => { + throw new InternalServerErrorRpcExcption(e); + }); + } + + async mkdir(path: string) { + const _path = join(STORE_DIR, path || ''); + return await this.assetHelper.mkdir(_path).catch((e) => { + throw new InternalServerErrorRpcExcption(e); + }); + } +} diff --git a/apps/store-service/tsconfig.app.json b/apps/store-service/tsconfig.app.json new file mode 100644 index 000000000..3463555ef --- /dev/null +++ b/apps/store-service/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/store-service" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/apps/themes-service/src/themes-service.service.ts b/apps/themes-service/src/themes-service.service.ts index 9aec766db..35581b104 100644 --- a/apps/themes-service/src/themes-service.service.ts +++ b/apps/themes-service/src/themes-service.service.ts @@ -422,7 +422,7 @@ export class ThemesServiceService { } fs.renameSync(join(_path, `${_theme}.bak`), join(_path, _theme)); consola.info(`主题 ${chalk.green(id)} 更新失败, 正在回滚`); - throw e; + throw new InternalServerErrorException(e); }); this.refreshThemes(); this.reloadConfig(id); diff --git a/libs/helper/src/helper.assets.service.ts b/libs/helper/src/helper.assets.service.ts index ce5eab78d..0b08b99ab 100644 --- a/libs/helper/src/helper.assets.service.ts +++ b/libs/helper/src/helper.assets.service.ts @@ -1,9 +1,11 @@ -import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import AdmZip from 'adm-zip'; import { HttpService } from './helper.http.service'; import fs from 'fs'; import { isURL } from 'class-validator'; import { tmpdir } from 'os'; +import { Readable } from 'stream'; +import { lookup } from 'mime-types'; @Injectable() export class AssetsService { @@ -12,7 +14,7 @@ export class AssetsService { async downloadZIPAndExtract(url: string, _path: string, name?: string) { // 1. Check if the URL is valid. if (!isURL(url)) { - throw new InternalServerErrorException('Invalid URL'); + throw new Error('Invalid URL'); } // 2. Download the ZIP file. const res = await this.http.axiosRef(url, { @@ -34,14 +36,14 @@ export class AssetsService { fs.renameSync(path.join(tmpdir(), zip.getEntries()[0].entryName), real); } catch { // fs.renameSync(path.join(tmpdir(), zip.getEntries()[0].entryName), real); - throw new InternalServerErrorException('当前主题已存在,正在跳过'); + throw new Error('当前文件已存在,正在跳过'); } return true; } async downloadFile(url: string, _path: string, name?: string) { if (!isURL(url)) { - throw new InternalServerErrorException('Invalid URL'); + throw new Error('Invalid URL'); } const res = await this.http.axiosRef(url, { responseType: 'arraybuffer', @@ -56,10 +58,74 @@ export class AssetsService { async writeFile(buffer: Buffer, _path: string, name: string) { fs.writeFileSync(path.join(_path, name), buffer); + return { + name, + } } async uploadZIPAndExtract(buffer: Buffer, _path: string, name?: string) { await this.extractZIP(buffer, _path, name); return true; } + + async deleteFile(path: string) { + if (path === '/') { + throw new Error('Cannot delete root directory'); + } + fs.rmSync(path, { recursive: true }); + return true; + } + + async getFile(_path: string) { + const file = fs.readFileSync(_path); + const name = _path.split('/').pop()!; + const ext = path.extname(name) + const mimetype = lookup(ext) + return { + file, + name, + ext, + mimetype + }; + } + + async getFileList(path: string) { + return fs.readdirSync(path, { withFileTypes: true, encoding: 'utf-8' }); + } + + async mkdir(path: string) { + fs.mkdirSync(path); + return true; + } + + exists(path: string) { + return fs.existsSync(path); + } + + async writeReadableFile( + readable: Readable, + _path: string, + name: string, + ): Promise { + return new Promise((resolve, reject) => { + const filePath = path.join(_path, name); + if (this.exists(filePath)) { + reject(new Error('File already exists')); + return; + } + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + const writable = fs.createWriteStream(filePath, { + encoding: 'utf-8', + }); + readable.pipe(writable); + writable.on('close', () => { + resolve(true); + }); + writable.on('error', () => reject(null)); + readable.on('end', () => { + writable.end(); + }); + readable.on('error', () => reject(null)); + }); + } } diff --git a/mog-core.code-workspace b/mog-core.code-workspace index 3f4410444..6a8bec29f 100644 --- a/mog-core.code-workspace +++ b/mog-core.code-workspace @@ -22,6 +22,9 @@ { "path": "apps/themes-service", }, + { + "path": "apps/store-service", + }, { "path": "libs" }, diff --git a/nest-cli.json b/nest-cli.json index f9107ff78..cc13fb576 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -128,6 +128,15 @@ "compilerOptions": { "tsConfigPath": "apps/themes-service/tsconfig.app.json" } + }, + "store-service": { + "type": "application", + "root": "apps/store-service", + "entryFile": "main", + "sourceRoot": "apps/store-service/src", + "compilerOptions": { + "tsConfigPath": "apps/store-service/tsconfig.app.json" + } } } } \ No newline at end of file diff --git a/package.json b/package.json index 58b4e66b9..ee67dafe4 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "bundle:friends-service": "cd dist/apps/friends-service/src && ncc build main.js -o ../../../../out/friends-service", "bundle:notification-service": "cd dist/apps/notification-service/src && ncc build main.js -o ../../../../out/notification-service", "bundle:themes-service": "cd dist/apps/themes-service/src && ncc build main.js -o ../../../../out/themes-service", + "bundle:store-service": "cd dist/apps/store-service/src && ncc build main.js -o ../../../../out/store-service", "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", "start": "concurrently \"npm:start:*\"", "start:core": "cross-env NODE_ENV=development cross-env NODE_ENV=development nest start -w core", @@ -32,6 +33,7 @@ "start:friends-service": "cross-env NODE_ENV=development nest start -w friends-service", "start:notification-service": "cross-env NODE_ENV=development nest start -w notification-service", "start:themes-service": "cross-env NODE_ENV=development nest start -w themes-service", + "start:store-service": "cross-env NODE_ENV=development nest start -w store-service", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "prod": "cross-env NODE_ENV=production pm2-runtime start ecosystem.config.js", "prod:pm2": "cross-env NODE_ENV=production pm2 restart ecosystem.config.js", @@ -77,6 +79,7 @@ "jsonwebtoken": "9.0.0", "lodash": "4.17.21", "mime": "^3.0.0", + "mime-types": "^2.1.35", "mongoose": "7.0.4", "mongoose-aggregate-paginate-v2": "1.0.6", "mongoose-lean-id": "0.5.0", @@ -108,6 +111,7 @@ "@types/jsonwebtoken": "^9.0.1", "@types/lodash": "4.14.194", "@types/mime": "^3.0.1", + "@types/mime-types": "^2.1.1", "@types/mongoose-aggregate-paginate-v2": "1.0.7", "@types/node": "18.15.11", "@types/supertest": "2.0.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4bd46c89..743491ff1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,6 +103,9 @@ dependencies: mime: specifier: ^3.0.0 version: 3.0.0 + mime-types: + specifier: ^2.1.35 + version: 2.1.35 mongoose: specifier: 7.0.4 version: 7.0.4 @@ -192,6 +195,9 @@ devDependencies: '@types/mime': specifier: ^3.0.1 version: 3.0.1 + '@types/mime-types': + specifier: ^2.1.1 + version: 2.1.1 '@types/mongoose-aggregate-paginate-v2': specifier: 1.0.7 version: 1.0.7 @@ -1434,6 +1440,10 @@ packages: resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==} dev: true + /@types/mime-types@2.1.1: + resolution: {integrity: sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==} + dev: true + /@types/mime@3.0.1: resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} dev: true diff --git a/shared/common/decorator/http.decorator.ts b/shared/common/decorator/http.decorator.ts index 59cc890da..183408ce2 100644 --- a/shared/common/decorator/http.decorator.ts +++ b/shared/common/decorator/http.decorator.ts @@ -39,4 +39,5 @@ export declare interface FileDecoratorProps { export const HTTPDecorators = { Paginator, Bypass, + FileUpload, }; diff --git a/shared/constants/event.constant.ts b/shared/constants/event.constant.ts index 15a670ae6..cb82c4a52 100644 --- a/shared/constants/event.constant.ts +++ b/shared/constants/event.constant.ts @@ -144,3 +144,13 @@ export enum ThemesEvents { ThemeBeforeUninstall = 'theme.beforeUninstall', ThemeAfterUninstall = 'theme.afterUninstall', } + +export enum StoreEvents { + Ping = 'store.ping', + StoreFileUploadByMaster = 'store.file.upload.auth', + StoreFileDownloadFromRemote = 'store.file.download.remote.auth', + StoreFileGet = 'store.file.get', + StoreFileDeleteByMaster = 'store.file.delete.auth', + StoreFileGetList = 'store.file.get.list', + StoreFileMkdirByMaster = 'store.file.mkdir.auth', +} \ No newline at end of file diff --git a/shared/constants/path.constant.ts b/shared/constants/path.constant.ts index f2c675599..bd041ce40 100644 --- a/shared/constants/path.constant.ts +++ b/shared/constants/path.constant.ts @@ -11,3 +11,4 @@ export const LOG_DIR = join(DATA_DIR, 'log'); export const BACKUP_DIR = join(DATA_DIR, 'backup'); export const THEME_DIR = join(DATA_DIR, 'themes'); export const PUBLIC_DIR = join(DATA_DIR, 'public'); +export const STORE_DIR = join(DATA_DIR, `store`); diff --git a/shared/constants/services.constant.ts b/shared/constants/services.constant.ts index ceaf4ff46..4bfc4af08 100644 --- a/shared/constants/services.constant.ts +++ b/shared/constants/services.constant.ts @@ -23,6 +23,7 @@ export enum ServicesEnum { mail = 'MAIL_SERVICE', notification = 'NOTIFICATION_SERVICE', custom = 'CUSTOM_SERVICE', + store = 'STORE_SERVICE', } export enum ServicePorts { diff --git a/shared/exceptions/internal-server-error-rpc-exception.ts b/shared/exceptions/internal-server-error-rpc-exception.ts new file mode 100644 index 000000000..fe5735604 --- /dev/null +++ b/shared/exceptions/internal-server-error-rpc-exception.ts @@ -0,0 +1,8 @@ +import { HttpStatus } from "@nestjs/common"; +import { RpcException } from "~/shared/exceptions/rpc-exception"; + +export class InternalServerErrorRpcExcption extends RpcException { + constructor(readonly message: string) { + super(message, HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/shared/global/index.global.ts b/shared/global/index.global.ts index d82c600b1..e2a92d8d6 100644 --- a/shared/global/index.global.ts +++ b/shared/global/index.global.ts @@ -7,6 +7,7 @@ import 'zx-cjs/globals'; import { DATA_DIR, LOG_DIR, + STORE_DIR, THEME_DIR, } from '@shared/constants/path.constant'; import { consola } from './consola.global'; @@ -39,8 +40,14 @@ export function mkThemeDir() { export function mkLogDir(service: string) { mkBasedirs(); - mkdirSync(join(LOG_DIR, `${service}_service`), { recursive: true }); - Logger.log(chalk.blue(`日志文件夹 已准备好: ${LOG_DIR}`)); + const dir = join(LOG_DIR, `${service}_service`); + mkdirSync(dir, { recursive: true }); + Logger.log(chalk.blue(`日志文件夹 已准备好: ${dir}`)); +} + +export function mkStoreDir() { + mkdirSync(STORE_DIR, { recursive: true }); + Logger.log(chalk.blue(`储藏文件夹 已准备好: ${STORE_DIR}`)); } export function registerGlobal() {