From 7a9f97595a03305d8a917d774fe3723b2a60b9b6 Mon Sep 17 00:00:00 2001 From: snowgitea Date: Wed, 1 Jul 2026 17:57:05 +0800 Subject: [PATCH] chore: src/modules/plugin/service/info.ts --- src/modules/plugin/service/info.ts | 474 +++++++++++++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 src/modules/plugin/service/info.ts diff --git a/src/modules/plugin/service/info.ts b/src/modules/plugin/service/info.ts new file mode 100644 index 0000000..5433812 --- /dev/null +++ b/src/modules/plugin/service/info.ts @@ -0,0 +1,474 @@ +import { + BaseService, + CoolCommException, + CoolEventManager, +} from '@cool-midway/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Equal, In, Not, Repository } from 'typeorm'; +import { PluginInfoEntity } from '../entity/info'; +import { + App, + Config, + ILogger, + IMidwayApplication, + IMidwayContext, + Inject, + InjectClient, + Logger, + Provide, +} from '@midwayjs/core'; +import * as _ from 'lodash'; +import { PluginInfo } from '../interface'; +import { PluginCenterService } from './center'; +import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager'; +import { + GLOBAL_EVENT_PLUGIN_INIT, + GLOBAL_EVENT_PLUGIN_REMOVE, +} from '../event/init'; +import { PluginMap, AnyString } from '../../../../typings/plugin'; +import { PluginTypesService } from './types'; +import * as path from 'path'; +import * as fs from 'fs'; +import { pPluginPath } from '../../../comm/path'; +/** + * 插件信息 + */ +@Provide() +export class PluginService extends BaseService { + @InjectEntityModel(PluginInfoEntity) + pluginInfoEntity: Repository; + + @Inject() + ctx: IMidwayContext; + + @App() + app: IMidwayApplication; + + @Inject() + pluginCenterService: PluginCenterService; + + @Config('module.plugin.hooks') + hooksConfig; + + @InjectClient(CachingFactory, 'default') + midwayCache: MidwayCache; + + @Inject() + coolEventManager: CoolEventManager; + + @Inject() + pluginTypesService: PluginTypesService; + + @Logger() + logger: ILogger; + + /** + * 新增或更新 + * @param param + * @param type + */ + async addOrUpdate(param: any, type?: 'add' | 'update') { + await super.addOrUpdate(param, type); + const info = await this.pluginInfoEntity + .createQueryBuilder('a') + .select(['a.id', 'a.keyName', 'a.status', 'a.hook']) + .where({ + id: Equal(param.id), + }) + .getOne(); + if (info.status == 1) { + await this.reInit(info.keyName); + } else { + await this.remove(info.keyName, !!info.hook); + } + } + + /** + * 重新初始化插件 + */ + async reInit(keyName: string) { + // 多进程发送全局事件,pm2下生效,本地开发则通过普通事件 + this.coolEventManager.globalEmit(GLOBAL_EVENT_PLUGIN_INIT, false, keyName); + } + + /** + * 移除插件 + * @param keyName + * @param isHook + */ + async remove(keyName: string, isHook = false) { + // 多进程发送全局事件,pm2下生效 + this.coolEventManager.globalEmit( + GLOBAL_EVENT_PLUGIN_REMOVE, + false, + keyName, + isHook + ); + this.pluginTypesService.deleteDtsFile(keyName); + } + + /** + * 删除不经过回收站 + * @param ids + */ + async delete(ids: any) { + const list = await this.pluginInfoEntity.findBy({ id: In(ids) }); + for (const item of list) { + await this.remove(item.keyName, !!item.hook); + // 删除文件 + await this.deleteData(item.keyName); + } + await this.pluginInfoEntity.delete(ids); + } + + /** + * 更新 + * @param param + */ + async update(param: any) { + const old = await this.pluginInfoEntity.findOne({ + where: { id: param.id }, + select: ['id', 'status', 'hook'], + }); + // 启用插件,禁用同名插件 + if (old.hook && param.status == 1 && old.status != param.status) { + await this.pluginInfoEntity.update( + { hook: old.hook, status: 1, id: Not(old.id) }, + { status: 0 } + ); + } + await super.update(param); + } + + /** + * 获得插件配置 + * @param key + */ + async getConfig(key: string) { + return this.pluginCenterService.pluginInfos.get(key)?.config; + } + + /** + * 调用插件 + * @param key 插件key + * @param method 方法 + * @param params 参数 + * @returns + */ + async invoke( + key: K | AnyString, + method: string, + ...params + ) { + // 实例 + const instance: any = await this.getInstance(key); + return await instance[method](...params); + } + + /** + * 获得插件实例 + * @param key + * @returns + */ + async getInstance( + key: K | AnyString + ): Promise { + const check = await this.checkStatus(key); + if (!check) throw new CoolCommException(`插件[${key}]不存在或已禁用`); + let instance; + const pluginInfo = this.pluginCenterService.pluginInfos.get(key); + if (pluginInfo.singleton) { + instance = this.pluginCenterService.plugins.get(key); + } else { + instance = new (await this.pluginCenterService.plugins.get(key))(); + await instance.init(pluginInfo, this.ctx, this.app, { + cache: this.midwayCache, + pluginService: this, + }); + } + return instance; + } + + /** + * 检查状态 + * @param key + */ + async checkStatus(key: string) { + if (Object.keys(this.hooksConfig).includes(key)) { + return true; + } + const info = await this.pluginInfoEntity + .createQueryBuilder('a') + .select(['a.id', 'a.status']) + .where({ status: 1, keyName: Equal(key) }) + .getOne(); + + return !!info; + } + + /** + * 检查 + * @param filePath + */ + async check(filePath: string) { + let data; + try { + data = await this.data(filePath); + } catch (e) { + return { + type: 0, + message: `插件信息不完整,请检查${data?.errorData || ''}`, + }; + } + const check = await this.pluginInfoEntity.findOne({ + where: { keyName: Equal(data.pluginJson.key) }, + select: ['id', 'hook', 'status'], + }); + if (check && !check.hook) { + return { + type: 1, + message: '插件已存在,继续安装将覆盖', + }; + } + if (check && check.hook && check.status == 1) { + return { + type: 2, + message: + '已存在同名Hook插件,你可以继续安装,但是多个相同的Hook插件只能同时开启一个', + }; + } + return { + type: 3, + message: '检查通过', + }; + } + + /** + * 获得插件数据 + * @param filePath + */ + async data(filePath: string): Promise<{ + pluginJson: any; + readme: string; + logo: string; + content: string; + tsContent: string; + errorData: string; + }> { + const AdmZip = require('adm-zip'); + const zip = new AdmZip(filePath); + const files = zip.getEntries(); + let errorData; + let pluginJson: PluginInfo, + readme: string, + logo: string, + content: string, + tsContent: string; + + try { + // 通用方法获取文件内容 + const getFileContent = ( + entryName: string, + encoding: 'utf-8' | 'base64' = 'utf-8' + ) => { + const file = _.find(files, { entryName }); + if (!file) { + throw new Error(`File ${entryName} not found`); + } + return file?.getData()?.toString(encoding); + }; + + errorData = 'plugin.json'; + pluginJson = JSON.parse(getFileContent('plugin.json')); + + errorData = 'readme'; + readme = getFileContent(pluginJson.readme); + + errorData = 'logo'; + logo = getFileContent(pluginJson.logo, 'base64'); + + errorData = 'content'; + content = getFileContent('src/index.js'); + + tsContent = getFileContent('source/index.ts'); + } catch (e) { + throw new CoolCommException('插件信息不完整'); + } + return { + pluginJson, + readme, + logo, + content, + tsContent, + errorData, + }; + } + + /** + * 安装插件 + * @param file 文件 + * @param force 是否强制安装 + */ + async install(filePath: string, force = false) { + const forceBool = typeof force === 'string' ? force === 'true' : force; + const checkResult = await this.check(filePath); + if (checkResult.type != 3 && !forceBool) { + return checkResult; + } + const { pluginJson, readme, logo, content, tsContent } = await this.data( + filePath + ); + if (pluginJson.key == 'plugin') { + throw new CoolCommException('插件key不能为plugin,请更换其他key'); + } + const check = await this.pluginInfoEntity.findOne({ + where: { keyName: Equal(pluginJson.key) }, + select: ['id', 'status', 'config'], + }); + const data = { + name: pluginJson.name, + keyName: pluginJson.key, + version: pluginJson.version, + author: pluginJson.author, + hook: pluginJson.hook, + readme, + logo, + content: { + type: 'comm', + data: content, + }, + tsContent: { + type: 'ts', + data: tsContent, + }, + description: pluginJson.description, + pluginJson, + config: pluginJson.config, + status: 1, + } as PluginInfoEntity; + // 存在同名插件,更新,保留配置 + if (check) { + await this.pluginInfoEntity.update(check.id, { + ...data, + status: check.status, + config: { + ...pluginJson.config, + ...check.config, + }, + }); + } else { + // 全新安装 + await this.pluginInfoEntity.insert(data); + } + // 保存插件内容 + await this.saveData( + { + content: { + type: 'comm', + data: content, + }, + tsContent: { + type: 'ts', + data: tsContent, + }, + }, + pluginJson.key + ); + this.pluginTypesService.generateDtsFile(pluginJson.key, tsContent); + // 初始化插件 + await this.reInit(pluginJson.key); + } + + /** + * 将插件内容保存到文件 + * @param content 内容 + * @param keyName 插件key + */ + async saveData( + data: { + content: { + type: 'comm' | 'module'; + data: string; + }; + tsContent: { + type: 'ts'; + data: string; + }; + }, + keyName: string + ) { + const filePath = this.pluginPath(keyName); + // 确保目录存在 + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + // 写入文件,如果存在则覆盖 + fs.writeFileSync(filePath, JSON.stringify(data, null, 0), { flag: 'w' }); + } + + /** + * 获得插件数据 + * @param keyName + * @returns + */ + async getData(keyName: string): Promise<{ + content: { + type: 'comm' | 'module'; + data: string; + }; + tsContent: { + type: 'ts'; + data: string; + }; + }> { + const filePath = this.pluginPath(keyName); + if (!fs.existsSync(filePath)) { + // 尝试从数据库中获取 + const info = await this.pluginInfoEntity.findOne({ + where: { keyName: Equal(keyName) }, + select: ['content', 'tsContent'], + }); + if (info) { + // 保存插件到文件 + this.saveData( + { + content: info.content, + tsContent: info.tsContent, + }, + keyName + ); + return { + content: info.content, + tsContent: info.tsContent, + }; + } else { + this.logger.warn( + `插件[${keyName}]文件不存在,请卸载后重新安装: ${filePath}` + ); + return; + } + } + return JSON.parse(await fs.promises.readFile(filePath, 'utf-8')); + } + + /** + * 删除插件 + * @param keyName + */ + async deleteData(keyName: string) { + const filePath = this.pluginPath(keyName); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } + + /** + * 获得插件路径 + * @param keyName + * @returns + */ + pluginPath(keyName: string) { + return path.join(pPluginPath(), `${keyName}`); + } +}