mirror of
https://github.com/certd/certd.git
synced 2026-04-24 04:17:25 +08:00
perf: 插件支持导入导出
This commit is contained in:
@@ -66,6 +66,22 @@ export async function SetDisabled(data: { id?: number; name?: string; type?: str
|
||||
});
|
||||
}
|
||||
|
||||
export async function ExportPlugin(id: number) {
|
||||
return await request({
|
||||
url: apiPrefix + "/export",
|
||||
method: "post",
|
||||
data: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function ImportPlugin(body: any) {
|
||||
return await request({
|
||||
url: apiPrefix + "/import",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
export type PluginConfigBean = {
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
|
||||
@@ -2,8 +2,8 @@ import * as api from "./api";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Ref, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||
import { Modal } from "ant-design-vue";
|
||||
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||
import { Modal, notification } from "ant-design-vue";
|
||||
//@ts-ignore
|
||||
import yaml from "js-yaml";
|
||||
|
||||
@@ -36,7 +36,74 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
|
||||
const selectedRowKeys: Ref<any[]> = ref([]);
|
||||
context.selectedRowKeys = selectedRowKeys;
|
||||
const { openCrudFormDialog } = useFormWrapper();
|
||||
|
||||
async function openImportDialog() {
|
||||
function createCrudOptions() {
|
||||
return {
|
||||
crudOptions: {
|
||||
columns: {
|
||||
content: {
|
||||
title: "插件文件",
|
||||
type: "text",
|
||||
form: {
|
||||
component: {
|
||||
name: "pem-input",
|
||||
vModel: "modelValue",
|
||||
textarea: {
|
||||
rows: 8,
|
||||
},
|
||||
},
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
helper: "选择插件文件",
|
||||
},
|
||||
},
|
||||
override: {
|
||||
title: "同名覆盖",
|
||||
type: "dict-switch",
|
||||
dict: dict({
|
||||
data: [
|
||||
{
|
||||
value: true,
|
||||
label: "覆盖",
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
label: "不覆盖",
|
||||
},
|
||||
],
|
||||
}),
|
||||
form: {
|
||||
value: false,
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
helper: "如果已有相同名称插件,直接覆盖",
|
||||
},
|
||||
},
|
||||
},
|
||||
form: {
|
||||
wrapper: {
|
||||
title: "导入插件",
|
||||
saveRemind: false,
|
||||
},
|
||||
afterSubmit() {
|
||||
notification.success({ message: "操作成功" });
|
||||
},
|
||||
async doSubmit({ form }: any) {
|
||||
return await api.ImportPlugin({
|
||||
...form,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const { crudOptions } = createCrudOptions();
|
||||
await openCrudFormDialog({ crudOptions });
|
||||
}
|
||||
return {
|
||||
crudOptions: {
|
||||
settings: {
|
||||
@@ -65,8 +132,18 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
buttons: {
|
||||
add: {
|
||||
show: true,
|
||||
icon: "ion:ios-add-circle-outline",
|
||||
text: "自定义插件",
|
||||
},
|
||||
import: {
|
||||
show: true,
|
||||
icon: "ion:cloud-upload-outline",
|
||||
text: "导入",
|
||||
type: "primary",
|
||||
async click() {
|
||||
await openImportDialog();
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
rowHandle: {
|
||||
@@ -85,10 +162,33 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
}),
|
||||
},
|
||||
remove: {
|
||||
order: 999,
|
||||
show: compute(({ row }) => {
|
||||
return row.type === "custom";
|
||||
}),
|
||||
},
|
||||
export: {
|
||||
text: null,
|
||||
icon: "ion:cloud-download-outline",
|
||||
title: "导出",
|
||||
type: "link",
|
||||
show: compute(({ row }) => {
|
||||
return row.type === "custom";
|
||||
}),
|
||||
async click({ row }) {
|
||||
//将文本内容,作为文件下载
|
||||
const content = await api.ExportPlugin(row.id);
|
||||
if (content) {
|
||||
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${row.name}.yaml`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
table: {
|
||||
@@ -182,8 +282,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
},
|
||||
form: {
|
||||
show: true,
|
||||
helper: "必须为英文,驼峰命名,类型作为前缀\n例如AliyunDeployToCDN\n插件一旦被使用,不要修改名称",
|
||||
rules: [{ required: true }],
|
||||
helper: "必须为英文或数字,驼峰命名,类型作为前缀\n例如AliyunDeployToCDN\n插件一旦被使用,不要修改名称",
|
||||
rules: [
|
||||
{ required: true },
|
||||
{
|
||||
type: "regexp",
|
||||
pattern: /^[a-zA-Z][a-zA-Z0-9]+$/,
|
||||
message: "必须为英文或数字,驼峰命名,类型作为前缀",
|
||||
},
|
||||
],
|
||||
},
|
||||
column: {
|
||||
width: 250,
|
||||
@@ -205,7 +312,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
form: {
|
||||
show: true,
|
||||
helper: "上传到插件商店时,将作为插件名称前缀,例如:greper/pluginName",
|
||||
rules: [{ required: true }],
|
||||
rules: [
|
||||
{ required: true },
|
||||
{
|
||||
type: "regexp",
|
||||
pattern: /^[a-zA-Z][a-zA-Z0-9]+$/,
|
||||
message: "必须为英文字母或数字",
|
||||
},
|
||||
],
|
||||
},
|
||||
column: {
|
||||
width: 200,
|
||||
|
||||
@@ -26,6 +26,11 @@ process.on('uncaughtException', error => {
|
||||
});
|
||||
|
||||
@Configuration({
|
||||
// detectorOptions: {
|
||||
// ignore: [
|
||||
// '**/plugins/**'
|
||||
// ]
|
||||
// },
|
||||
imports: [
|
||||
koa,
|
||||
orm,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
|
||||
import { merge } from 'lodash-es';
|
||||
import { CrudController } from '@certd/lib-server';
|
||||
import { PluginService } from '../../../modules/plugin/service/plugin-service.js';
|
||||
import { PluginImportReq, PluginService } from "../../../modules/plugin/service/plugin-service.js";
|
||||
import { CommPluginConfig, PluginConfigService } from '../../../modules/plugin/service/plugin-config-service.js';
|
||||
/**
|
||||
* 插件
|
||||
@@ -82,4 +82,17 @@ export class PluginController extends CrudController<PluginService> {
|
||||
const res = await this.pluginConfigService.saveCommPluginConfig(body);
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
|
||||
@Post('/import', { summary: 'sys:settings:edit' })
|
||||
async import(@Body(ALL) body: PluginImportReq) {
|
||||
const res = await this.service.importPlugin(body);
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post('/export', { summary: 'sys:settings:edit' })
|
||||
async export(@Body('id') id: number) {
|
||||
const res = await this.service.exportPlugin(id);
|
||||
return this.ok(res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ export type PluginFindReq = {
|
||||
name?: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class PluginConfigService {
|
||||
|
||||
@@ -12,6 +12,10 @@ import { logger } from "@certd/basic";
|
||||
import yaml from "js-yaml";
|
||||
import { getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin } from "./default-plugin.js";
|
||||
|
||||
export type PluginImportReq = {
|
||||
content: string,
|
||||
override?: boolean;
|
||||
};
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
@@ -41,7 +45,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
const builtInList = await this.getBuiltInEntityList();
|
||||
|
||||
//获取分页数据
|
||||
const data = builtInList.slice(offset, offset + limit);
|
||||
const data = builtInList.slice(offset, offset + limit);
|
||||
|
||||
return {
|
||||
records: data,
|
||||
@@ -53,7 +57,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
|
||||
async getEnabledBuildInGroup(isSimple = false) {
|
||||
const groups = this.builtInPluginService.getGroups();
|
||||
if(isSimple){
|
||||
if (isSimple) {
|
||||
for (const key in groups) {
|
||||
const group = groups[key];
|
||||
group.plugins.forEach(item => {
|
||||
@@ -97,8 +101,8 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
});
|
||||
const disabledNames = list.map(it => it.name);
|
||||
|
||||
return builtInList.filter(it =>{
|
||||
return !disabledNames.includes(it.name)
|
||||
return builtInList.filter(it => {
|
||||
return !disabledNames.includes(it.name);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -168,33 +172,48 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
name: param.name,
|
||||
author: param.author
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (old) {
|
||||
throw new Error(`插件${param.author}/${param.name}已存在`);
|
||||
}
|
||||
|
||||
let plugin:any = {}
|
||||
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{
|
||||
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}不支持`);
|
||||
}
|
||||
|
||||
return await super.add({
|
||||
return await super.add({
|
||||
...param,
|
||||
...plugin
|
||||
});
|
||||
}
|
||||
|
||||
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}已存在`);
|
||||
}
|
||||
|
||||
return await super.update(param);
|
||||
}
|
||||
|
||||
async compile(code: string) {
|
||||
const ts = await import("typescript")
|
||||
const ts = await import("typescript");
|
||||
return ts.transpileModule(code, {
|
||||
compilerOptions: { module: ts.ModuleKind.ESNext }
|
||||
}).outputText;
|
||||
@@ -220,16 +239,16 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
if (info && info.length > 0) {
|
||||
const plugin = info[0];
|
||||
|
||||
try{
|
||||
try {
|
||||
const AsyncFunction = Object.getPrototypeOf(async () => {
|
||||
}).constructor;
|
||||
// const script = await this.compile(plugin.content);
|
||||
const script = plugin.content
|
||||
const script = plugin.content;
|
||||
const getPluginClass = new AsyncFunction(script);
|
||||
return await getPluginClass({ logger: logger });
|
||||
}catch (e) {
|
||||
logger.error("编译插件失败:",e)
|
||||
throw e
|
||||
} catch (e) {
|
||||
logger.error("编译插件失败:", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -284,4 +303,80 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
});
|
||||
}
|
||||
|
||||
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: req.content,
|
||||
disabled: false
|
||||
};
|
||||
if (!pluginEntity.pluginType) {
|
||||
throw new Error(`插件类型不能为空`);
|
||||
}
|
||||
|
||||
if (old) {
|
||||
if (!req.override) {
|
||||
throw new Error(`插件${loaded.author}/${loaded.name}已存在`);
|
||||
}
|
||||
//update
|
||||
pluginEntity.id = old.id;
|
||||
await this.update(pluginEntity);
|
||||
} else {
|
||||
//add
|
||||
const { id } = await this.add(pluginEntity);
|
||||
pluginEntity.id = id;
|
||||
}
|
||||
return {
|
||||
id: pluginEntity.id
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user