mirror of https://github.com/certd/certd
perf: 插件支持导入导出
parent
d66de26de4
commit
cf8abb4528
|
@ -1,7 +1,4 @@
|
||||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||||
import { ConnectConfig } from "ssh2";
|
|
||||||
import { SshClient } from "./ssh.js";
|
|
||||||
|
|
||||||
@IsAccess({
|
@IsAccess({
|
||||||
name: "ssh",
|
name: "ssh",
|
||||||
title: "主机登录授权",
|
title: "主机登录授权",
|
||||||
|
@ -9,7 +6,7 @@ import { SshClient } from "./ssh.js";
|
||||||
icon: "clarity:host-line",
|
icon: "clarity:host-line",
|
||||||
input: {},
|
input: {},
|
||||||
})
|
})
|
||||||
export class SshAccess extends BaseAccess implements ConnectConfig {
|
export class SshAccess extends BaseAccess {
|
||||||
@AccessInput({
|
@AccessInput({
|
||||||
title: "主机地址",
|
title: "主机地址",
|
||||||
component: {
|
component: {
|
||||||
|
@ -125,6 +122,7 @@ export class SshAccess extends BaseAccess implements ConnectConfig {
|
||||||
testRequest = true;
|
testRequest = true;
|
||||||
|
|
||||||
async onTestRequest() {
|
async onTestRequest() {
|
||||||
|
const { SshClient } = await import("./ssh.js");
|
||||||
const client = new SshClient(this.ctx.logger);
|
const client = new SshClient(this.ctx.logger);
|
||||||
|
|
||||||
await client.exec({
|
await client.exec({
|
||||||
|
|
|
@ -1,22 +1,33 @@
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import ssh2, { ConnectConfig, ExecOptions } from "ssh2";
|
|
||||||
|
|
||||||
import ssh2Constants from "ssh2/lib/protocol/constants.js";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import * as _ from "lodash-es";
|
import { isArray } from "lodash-es";
|
||||||
import { ILogger } from "@certd/basic";
|
import { ILogger } from "@certd/basic";
|
||||||
import { SshAccess } from "./ssh-access.js";
|
import { SshAccess } from "./ssh-access.js";
|
||||||
import stripAnsi from "strip-ansi";
|
|
||||||
import { SocksClient } from "socks";
|
|
||||||
import { SocksProxy, SocksProxyType } from "socks/typings/common/constants.js";
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import { SocksProxyType } from "socks/typings/common/constants";
|
||||||
|
|
||||||
export type TransportItem = { localPath: string; remotePath: string };
|
export type TransportItem = { localPath: string; remotePath: string };
|
||||||
|
export interface SocksProxy {
|
||||||
|
ipaddress?: string;
|
||||||
|
host?: string;
|
||||||
|
port: number;
|
||||||
|
type: any;
|
||||||
|
userId?: string;
|
||||||
|
password?: string;
|
||||||
|
custom_auth_method?: number;
|
||||||
|
custom_auth_request_handler?: () => Promise<Buffer>;
|
||||||
|
custom_auth_response_size?: number;
|
||||||
|
custom_auth_response_handler?: (data: Buffer) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
export type SshConnectConfig = {
|
||||||
|
sock?: any;
|
||||||
|
};
|
||||||
|
|
||||||
export class AsyncSsh2Client {
|
export class AsyncSsh2Client {
|
||||||
conn: ssh2.Client;
|
conn: any;
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
connConf: SshAccess & ssh2.ConnectConfig;
|
connConf: SshAccess & SshConnectConfig;
|
||||||
windows = false;
|
windows = false;
|
||||||
encoding: string;
|
encoding: string;
|
||||||
constructor(connConf: SshAccess, logger: ILogger) {
|
constructor(connConf: SshAccess, logger: ILogger) {
|
||||||
|
@ -40,7 +51,10 @@ export class AsyncSsh2Client {
|
||||||
if (typeof this.connConf.port === "string") {
|
if (typeof this.connConf.port === "string") {
|
||||||
this.connConf.port = parseInt(this.connConf.port);
|
this.connConf.port = parseInt(this.connConf.port);
|
||||||
}
|
}
|
||||||
const proxyOption: SocksProxy = this.parseSocksProxyFromUri(this.connConf.socksProxy);
|
|
||||||
|
const { SocksClient } = await import("socks");
|
||||||
|
|
||||||
|
const proxyOption = this.parseSocksProxyFromUri(this.connConf.socksProxy);
|
||||||
const info = await SocksClient.createConnection({
|
const info = await SocksClient.createConnection({
|
||||||
proxy: proxyOption,
|
proxy: proxyOption,
|
||||||
command: "connect",
|
command: "connect",
|
||||||
|
@ -53,10 +67,12 @@ export class AsyncSsh2Client {
|
||||||
this.connConf.sock = info.socket;
|
this.connConf.sock = info.socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { SUPPORTED_KEX, SUPPORTED_SERVER_HOST_KEY, SUPPORTED_CIPHER, SUPPORTED_MAC } = ssh2Constants;
|
const ssh2 = await import("ssh2");
|
||||||
|
const ssh2Constants = await import("ssh2/lib/protocol/constants.js");
|
||||||
|
const { SUPPORTED_KEX, SUPPORTED_SERVER_HOST_KEY, SUPPORTED_CIPHER, SUPPORTED_MAC } = ssh2Constants.default;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const conn = new ssh2.Client();
|
const conn = new ssh2.default.Client();
|
||||||
conn
|
conn
|
||||||
.on("error", (err: any) => {
|
.on("error", (err: any) => {
|
||||||
this.logger.error("连接失败", err);
|
this.logger.error("连接失败", err);
|
||||||
|
@ -197,6 +213,8 @@ export class AsyncSsh2Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
async shell(script: string | string[]): Promise<string> {
|
async shell(script: string | string[]): Promise<string> {
|
||||||
|
const stripAnsiModule = await import("strip-ansi");
|
||||||
|
const stripAnsi = stripAnsiModule.default;
|
||||||
return new Promise<any>((resolve, reject) => {
|
return new Promise<any>((resolve, reject) => {
|
||||||
this.logger.info(`执行shell脚本:[${this.connConf.host}][shell]: ` + script);
|
this.logger.info(`执行shell脚本:[${this.connConf.host}][shell]: ` + script);
|
||||||
this.conn.shell((err: Error, stream: any) => {
|
this.conn.shell((err: Error, stream: any) => {
|
||||||
|
@ -449,7 +467,7 @@ export class SshClient {
|
||||||
script = script.join(" && ");
|
script = script.join(" && ");
|
||||||
} else {
|
} else {
|
||||||
const newLine = isLinux ? "\n" : "\r\n";
|
const newLine = isLinux ? "\n" : "\r\n";
|
||||||
if (_.isArray(script)) {
|
if (isArray(script)) {
|
||||||
script = script as Array<string>;
|
script = script as Array<string>;
|
||||||
script = script.join(newLine);
|
script = script.join(newLine);
|
||||||
}
|
}
|
||||||
|
@ -465,7 +483,7 @@ export class SshClient {
|
||||||
async shell(options: { connectConf: SshAccess; script: string | Array<string> }): Promise<string> {
|
async shell(options: { connectConf: SshAccess; script: string | Array<string> }): Promise<string> {
|
||||||
let { script } = options;
|
let { script } = options;
|
||||||
const { connectConf } = options;
|
const { connectConf } = options;
|
||||||
if (_.isArray(script)) {
|
if (isArray(script)) {
|
||||||
script = script as Array<string>;
|
script = script as Array<string>;
|
||||||
if (connectConf.windows) {
|
if (connectConf.windows) {
|
||||||
script = script.join("\r\n");
|
script = script.join("\r\n");
|
||||||
|
|
|
@ -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 = {
|
export type PluginConfigBean = {
|
||||||
name: string;
|
name: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
|
|
@ -2,8 +2,8 @@ import * as api from "./api";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { Ref, ref } from "vue";
|
import { Ref, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||||
import { Modal } from "ant-design-vue";
|
import { Modal, notification } from "ant-design-vue";
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
@ -36,7 +36,74 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||||
|
|
||||||
const selectedRowKeys: Ref<any[]> = ref([]);
|
const selectedRowKeys: Ref<any[]> = ref([]);
|
||||||
context.selectedRowKeys = selectedRowKeys;
|
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 {
|
return {
|
||||||
crudOptions: {
|
crudOptions: {
|
||||||
settings: {
|
settings: {
|
||||||
|
@ -65,8 +132,18 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||||
buttons: {
|
buttons: {
|
||||||
add: {
|
add: {
|
||||||
show: true,
|
show: true,
|
||||||
|
icon: "ion:ios-add-circle-outline",
|
||||||
text: "自定义插件",
|
text: "自定义插件",
|
||||||
},
|
},
|
||||||
|
import: {
|
||||||
|
show: true,
|
||||||
|
icon: "ion:cloud-upload-outline",
|
||||||
|
text: "导入",
|
||||||
|
type: "primary",
|
||||||
|
async click() {
|
||||||
|
await openImportDialog();
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rowHandle: {
|
rowHandle: {
|
||||||
|
@ -85,10 +162,33 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
remove: {
|
remove: {
|
||||||
|
order: 999,
|
||||||
show: compute(({ row }) => {
|
show: compute(({ row }) => {
|
||||||
return row.type === "custom";
|
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: {
|
table: {
|
||||||
|
@ -182,8 +282,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
show: true,
|
show: true,
|
||||||
helper: "必须为英文,驼峰命名,类型作为前缀\n例如AliyunDeployToCDN\n插件一旦被使用,不要修改名称",
|
helper: "必须为英文或数字,驼峰命名,类型作为前缀\n例如AliyunDeployToCDN\n插件一旦被使用,不要修改名称",
|
||||||
rules: [{ required: true }],
|
rules: [
|
||||||
|
{ required: true },
|
||||||
|
{
|
||||||
|
type: "regexp",
|
||||||
|
pattern: /^[a-zA-Z][a-zA-Z0-9]+$/,
|
||||||
|
message: "必须为英文或数字,驼峰命名,类型作为前缀",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
column: {
|
column: {
|
||||||
width: 250,
|
width: 250,
|
||||||
|
@ -205,7 +312,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||||
form: {
|
form: {
|
||||||
show: true,
|
show: true,
|
||||||
helper: "上传到插件商店时,将作为插件名称前缀,例如:greper/pluginName",
|
helper: "上传到插件商店时,将作为插件名称前缀,例如:greper/pluginName",
|
||||||
rules: [{ required: true }],
|
rules: [
|
||||||
|
{ required: true },
|
||||||
|
{
|
||||||
|
type: "regexp",
|
||||||
|
pattern: /^[a-zA-Z][a-zA-Z0-9]+$/,
|
||||||
|
message: "必须为英文字母或数字",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
column: {
|
column: {
|
||||||
width: 200,
|
width: 200,
|
||||||
|
|
|
@ -26,6 +26,11 @@ process.on('uncaughtException', error => {
|
||||||
});
|
});
|
||||||
|
|
||||||
@Configuration({
|
@Configuration({
|
||||||
|
// detectorOptions: {
|
||||||
|
// ignore: [
|
||||||
|
// '**/plugins/**'
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
imports: [
|
imports: [
|
||||||
koa,
|
koa,
|
||||||
orm,
|
orm,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
|
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
|
||||||
import { merge } from 'lodash-es';
|
import { merge } from 'lodash-es';
|
||||||
import { CrudController } from '@certd/lib-server';
|
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';
|
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);
|
const res = await this.pluginConfigService.saveCommPluginConfig(body);
|
||||||
return this.ok(res);
|
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;
|
name?: string;
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||||
export class PluginConfigService {
|
export class PluginConfigService {
|
||||||
|
|
|
@ -12,6 +12,10 @@ import { logger } from "@certd/basic";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin } from "./default-plugin.js";
|
import { getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin } from "./default-plugin.js";
|
||||||
|
|
||||||
|
export type PluginImportReq = {
|
||||||
|
content: string,
|
||||||
|
override?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||||
|
@ -41,7 +45,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||||
const builtInList = await this.getBuiltInEntityList();
|
const builtInList = await this.getBuiltInEntityList();
|
||||||
|
|
||||||
//获取分页数据
|
//获取分页数据
|
||||||
const data = builtInList.slice(offset, offset + limit);
|
const data = builtInList.slice(offset, offset + limit);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
records: data,
|
records: data,
|
||||||
|
@ -53,7 +57,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||||
|
|
||||||
async getEnabledBuildInGroup(isSimple = false) {
|
async getEnabledBuildInGroup(isSimple = false) {
|
||||||
const groups = this.builtInPluginService.getGroups();
|
const groups = this.builtInPluginService.getGroups();
|
||||||
if(isSimple){
|
if (isSimple) {
|
||||||
for (const key in groups) {
|
for (const key in groups) {
|
||||||
const group = groups[key];
|
const group = groups[key];
|
||||||
group.plugins.forEach(item => {
|
group.plugins.forEach(item => {
|
||||||
|
@ -97,8 +101,8 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||||
});
|
});
|
||||||
const disabledNames = list.map(it => it.name);
|
const disabledNames = list.map(it => it.name);
|
||||||
|
|
||||||
return builtInList.filter(it =>{
|
return builtInList.filter(it => {
|
||||||
return !disabledNames.includes(it.name)
|
return !disabledNames.includes(it.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,33 +172,48 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||||
name: param.name,
|
name: param.name,
|
||||||
author: param.author
|
author: param.author
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
if (old) {
|
if (old) {
|
||||||
throw new Error(`插件${param.author}/${param.name}已存在`);
|
throw new Error(`插件${param.author}/${param.name}已存在`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let plugin:any = {}
|
let plugin: any = {};
|
||||||
if (param.pluginType === "access") {
|
if (param.pluginType === "access") {
|
||||||
plugin = getDefaultAccessPlugin()
|
plugin = getDefaultAccessPlugin();
|
||||||
delete param.group
|
delete param.group;
|
||||||
}else if (param.pluginType === "deploy") {
|
} else if (param.pluginType === "deploy") {
|
||||||
plugin = getDefaultDeployPlugin()
|
plugin = getDefaultDeployPlugin();
|
||||||
}else if (param.pluginType === "dnsProvider") {
|
} else if (param.pluginType === "dnsProvider") {
|
||||||
plugin = getDefaultDnsPlugin()
|
plugin = getDefaultDnsPlugin();
|
||||||
delete param.group
|
delete param.group;
|
||||||
}else{
|
} else {
|
||||||
throw new Error(`插件类型${param.pluginType}不支持`);
|
throw new Error(`插件类型${param.pluginType}不支持`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await super.add({
|
return await super.add({
|
||||||
...param,
|
...param,
|
||||||
...plugin
|
...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) {
|
async compile(code: string) {
|
||||||
const ts = await import("typescript")
|
const ts = await import("typescript");
|
||||||
return ts.transpileModule(code, {
|
return ts.transpileModule(code, {
|
||||||
compilerOptions: { module: ts.ModuleKind.ESNext }
|
compilerOptions: { module: ts.ModuleKind.ESNext }
|
||||||
}).outputText;
|
}).outputText;
|
||||||
|
@ -220,16 +239,16 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||||
if (info && info.length > 0) {
|
if (info && info.length > 0) {
|
||||||
const plugin = info[0];
|
const plugin = info[0];
|
||||||
|
|
||||||
try{
|
try {
|
||||||
const AsyncFunction = Object.getPrototypeOf(async () => {
|
const AsyncFunction = Object.getPrototypeOf(async () => {
|
||||||
}).constructor;
|
}).constructor;
|
||||||
// const script = await this.compile(plugin.content);
|
// const script = await this.compile(plugin.content);
|
||||||
const script = plugin.content
|
const script = plugin.content;
|
||||||
const getPluginClass = new AsyncFunction(script);
|
const getPluginClass = new AsyncFunction(script);
|
||||||
return await getPluginClass({ logger: logger });
|
return await getPluginClass({ logger: logger });
|
||||||
}catch (e) {
|
} catch (e) {
|
||||||
logger.error("编译插件失败:",e)
|
logger.error("编译插件失败:", e);
|
||||||
throw 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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue