chore: src/modules/plugin/service/info.ts

This commit is contained in:
2026-07-01 17:57:05 +08:00
parent 077eb3a7cd
commit 7a9f97595a
+474
View File
@@ -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<PluginInfoEntity>;
@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<K extends keyof PluginMap>(
key: K | AnyString,
method: string,
...params
) {
// 实例
const instance: any = await this.getInstance(key);
return await instance[method](...params);
}
/**
* 获得插件实例
* @param key
* @returns
*/
async getInstance<K extends keyof PluginMap>(
key: K | AnyString
): Promise<K extends keyof PluginMap ? PluginMap[K] : any> {
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}`);
}
}