import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { BaseService, PageReq } from "@certd/lib-server"; import { PluginEntity } from "../entity/plugin.js"; import { InjectEntityModel } from "@midwayjs/typeorm"; import { Repository } from "typeorm"; import { isComm } from "@certd/plus-core"; import { BuiltInPluginService } from "../../pipeline/service/builtin-plugin-service.js"; import { merge } from "lodash-es"; import { accessRegistry, notificationRegistry, pluginRegistry } from "@certd/pipeline"; import { dnsProviderRegistry } from "@certd/plugin-cert"; import { logger } from "@certd/basic"; import yaml from "js-yaml"; import { getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin } from "./default-plugin.js"; import fs from "fs"; import path from "path"; export type PluginImportReq = { content: string, override?: boolean; }; @Provide() @Scope(ScopeEnum.Request, {allowDowngrade: true}) export class PluginService extends BaseService { @InjectEntityModel(PluginEntity) repository: Repository; @Inject() builtInPluginService: BuiltInPluginService; //@ts-ignore getRepository() { return this.repository; } // eslint-disable-next-line @typescript-eslint/no-unused-vars async page(pageReq: PageReq) { if (pageReq.query.type && pageReq.query.type !== "builtIn") { return await super.page(pageReq); } //仅查询内置插件 const offset = pageReq.page.offset; const limit = pageReq.page.limit; const builtInList = await this.getBuiltInEntityList(); //获取分页数据 const data = builtInList.slice(offset, offset + limit); return { records: data, total: builtInList.length, offset: offset, limit: limit }; } async getEnabledBuildInGroup(isSimple = false) { const groups = this.builtInPluginService.getGroups(); if (isSimple) { for (const key in groups) { const group = groups[key]; group.plugins.forEach(item => { delete item.input; }); } } if (!isComm()) { return groups; } const list = await this.list({ query: { type: "builtIn", disabled: true } }); const disabledNames = list.map(it => it.name); for (const key in groups) { const group = groups[key]; if (!group.plugins) { continue; } group.plugins = group.plugins.filter(it => !disabledNames.includes(it.name)); } return groups; } async getEnabledBuiltInList(): Promise { const builtInList = this.builtInPluginService.getList(); if (!isComm()) { return builtInList; } const list = await this.list({ query: { type: "builtIn", disabled: true } }); const disabledNames = list.map(it => it.name); return builtInList.filter(it => { return !disabledNames.includes(it.name); }); } async getBuiltInEntityList() { const builtInList = this.builtInPluginService.getList(); const list = await this.list({ query: { type: "builtIn" } }); const records: PluginEntity[] = []; for (const item of builtInList) { let record = list.find(it => it.name === item.name); if (!record) { record = new PluginEntity(); record.disabled = false; } merge(record, { name: item.name, title: item.title, type: "builtIn", icon: item.icon, desc: item.desc, group: item.group }); records.push(record); } return records; } async setDisabled(opts: { id?: number; name?: string; type: string; disabled: boolean }) { const {id, name, type, disabled} = opts; if (!type) { throw new Error("参数错误: type 不能为空"); } if (id > 0) { //update await this.repository.update({id}, {disabled}); return; } if (name && type === "builtIn") { const pluginEntity = new PluginEntity(); pluginEntity.name = name; pluginEntity.type = type; pluginEntity.disabled = disabled; await this.repository.save(pluginEntity); return; } throw new Error("参数错误: id 和 name 必须有一个"); } async getDefineByType(type: string) { return this.builtInPluginService.getByType(type); } /** * 新增 * @param param 数据 */ async add(param: any) { const old = await this.repository.findOne({ where: { name: param.name, author: param.author } }); if (old) { throw new Error(`插件${param.author}/${param.name}已存在`); } let plugin: any = {}; if (param.pluginType === "access") { plugin = getDefaultAccessPlugin(); delete param.group; } else if (param.pluginType === "deploy") { plugin = getDefaultDeployPlugin(); } else if (param.pluginType === "dnsProvider") { plugin = getDefaultDnsPlugin(); delete param.group; } else { throw new Error(`插件类型${param.pluginType}不支持`); } const res= await super.add({ ...param, ...plugin }); await this.registerById(res.id); return res } async registerById(id: any) { const item = await this.info(id); if (!item) { return; } if(item.type === "builtIn"){ return; } await this.registerPlugin(item); } async unRegisterById(id: any){ const item = await this.info(id); if (!item) { return; } if (item.type === "builtIn") { return; } let name = item.name; if (item.author){ name = `${item.author}/${item.name}` } if (item.pluginType === "access"){ accessRegistry.unRegister(name) }else if (item.pluginType === "deploy"){ pluginRegistry.unRegister(name) }else if (item.pluginType === "dnsProvider"){ dnsProviderRegistry.unRegister(name) }else if (item.pluginType === "notification"){ notificationRegistry.unRegister(name) }else{ logger.warn(`不支持的插件类型:${item.pluginType}`) } } async update(param: any) { const old = await this.repository.findOne({ where: { name: param.name, author: param.author } }); if (old && old.id !== param.id) { throw new Error(`插件${param.author}/${param.name}已存在`); } const res= await super.update(param); await this.registerById(param.id); return res } async compile(code: string) { const ts = await import("typescript"); return ts.transpileModule(code, { compilerOptions: {module: ts.ModuleKind.ESNext} }).outputText; } private async getPluginClassFromFile(item: any) { const scriptFilePath = item.scriptFilePath; const res = await import((`${scriptFilePath}`)) const classNames = Object.keys(res) return res[classNames[0]] } async getPluginClassFromDb(pluginName: string) { //获取插件类实例对象 let author = undefined; let name = ""; if (pluginName.includes("/")) { const arr = pluginName.split("/"); author = arr[0]; name = arr[1]; } else { name = pluginName; } const info = await this.find({ where: { name: name, author: author } }); if (info && info.length > 0) { const plugin = info[0]; try { const AsyncFunction = Object.getPrototypeOf(async () => { }).constructor; // const script = await this.compile(plugin.content); const script = plugin.content; const getPluginClass = new AsyncFunction(script); return await getPluginClass({logger: logger}); } catch (e) { logger.error("编译插件失败:", e); throw e; } } throw new Error(`插件${pluginName}不存在`); } /** * 从数据库加载插件 */ async registerFromDb() { const res = await this.list({ buildQuery: ((bq) => { bq.andWhere("type != :type", { type: "builtIn" }); }) }); for (const item of res) { await this.registerPlugin(item); } } async registerFromLocal(localDir: string) { //scan path const files = fs.readdirSync(localDir); let list = [] for (const file of files) { if (!file.endsWith(".yaml")) { continue; } const item = yaml.load(fs.readFileSync(path.join(localDir, file), "utf8")); list.push(item); } //排序 list = list.sort((a, b) => { return (a.order??10) - (b.order ??10); }); for (const item of list) { await this.registerPlugin(item); } } async registerPlugin(plugin: PluginEntity) { const metadata = plugin.metadata ? yaml.load(plugin.metadata) : {}; const extra = plugin.extra ? yaml.load(plugin.extra) : {}; const item = { ...plugin, ...metadata, ...extra }; delete item.metadata; delete item.content; delete item.extra; if (item.author) { item.name = item.author + "/" + item.name; } let registry = null; if (item.pluginType === "access") { registry = accessRegistry; } else if (item.pluginType === "deploy") { registry = pluginRegistry; } else if (item.pluginType === "dnsProvider") { registry = dnsProviderRegistry; } else if (item.pluginType === "notification") { registry = notificationRegistry; } else { logger.warn(`插件${item.name}类型错误:${item.pluginType}`); return; } registry.register(item.name, { define: item, target: async () => { if (item.type === "builtIn") { return await this.getPluginClassFromFile(item); } else { return await this.getPluginClassFromDb(item.name); } } }); } async exportPlugin(id: number) { const info = await this.info(id); if (!info) { throw new Error("插件不存在"); } const metadata = yaml.load(info.metadata || ""); const extra = yaml.load(info.extra || ""); const content = info.content; delete info.metadata; delete info.extra; delete info.content; delete info.id; delete info.createTime; delete info.updateTime; const plugin = { ...info, ...metadata, ...extra, content }; return yaml.dump(plugin) as string; } async importPlugin(req: PluginImportReq) { const loaded = yaml.load(req.content); if (!loaded) { throw new Error("插件内容不能为空"); } delete loaded.id const old = await this.repository.findOne({ where: { name: loaded.name, author: loaded.author } }); const metadata = { input: loaded.input, output: loaded.output }; const extra = { dependPlugins: loaded.dependPlugins, default: loaded.default, showRunStrategy: loaded.showRunStrategy }; const pluginEntity = { ...loaded, metadata: yaml.dump(metadata), extra: yaml.dump(extra), content: loaded.content, disabled: false }; if (!pluginEntity.pluginType) { throw new Error(`插件类型不能为空`); } if (!old) { //add const {id} = await this.add(pluginEntity); pluginEntity.id = id; } else{ if (!req.override) { throw new Error(`插件${loaded.author}/${loaded.name}已存在`); } pluginEntity.id = old.id; } //update await this.update(pluginEntity); return { id: pluginEntity.id }; } async deleteByIds(ids:any[]){ await super.delete(ids); for (const id of ids) { await this.unRegisterById(id) } } }