perf: 商业版支持自定义插件的参数配置

v2-dev-plugin-config
xiaojunnuo 2025-08-27 18:23:24 +08:00
parent 8e3d699856
commit 17f23f3751
13 changed files with 265 additions and 106 deletions

View File

@ -564,7 +564,7 @@ export default {
ipv6Priority: "IPv6 Priority", ipv6Priority: "IPv6 Priority",
dualStackNetworkHelper: "If IPv6 priority is selected, enable IPv6 in docker-compose.yaml", dualStackNetworkHelper: "If IPv6 priority is selected, enable IPv6 in docker-compose.yaml",
enableCommonCnameService: "Enable Public CNAME Service", enableCommonCnameService: "Enable Public CNAME Service",
commonCnameHelper: "Allow use of public CNAME service. If disabled and no <router-link to='/sys/cname/provider'>custom CNAME service</router-link> is set, CNAME proxy certificate application will not work.", commonCnameHelper: "Allow use of public CNAME service. If disabled and no <a href='#/sys/cname/provider'>custom CNAME service</a> is set, CNAME proxy certificate application will not work.",
enableCommonSelfServicePasswordRetrieval: "Enable self-service password recovery", enableCommonSelfServicePasswordRetrieval: "Enable self-service password recovery",
saveButton: "Save", saveButton: "Save",
stopSuccess: "Stopped successfully", stopSuccess: "Stopped successfully",

View File

@ -570,7 +570,7 @@ export default {
ipv6Priority: "IPV6优先", ipv6Priority: "IPV6优先",
dualStackNetworkHelper: "如果选择IPv6优先需要在docker-compose.yaml中启用ipv6", dualStackNetworkHelper: "如果选择IPv6优先需要在docker-compose.yaml中启用ipv6",
enableCommonCnameService: "启用公共CNAME服务", enableCommonCnameService: "启用公共CNAME服务",
commonCnameHelper: "是否可以使用公共CNAME服务如果禁用且没有设置<router-link to='/sys/cname/provider'>自定义CNAME服务</router-link>则无法使用CNAME代理方式申请证书", commonCnameHelper: "是否可以使用公共CNAME服务如果禁用且没有设置<a href='#/sys/cname/provider'>自定义CNAME服务</a>则无法使用CNAME代理方式申请证书",
enableCommonSelfServicePasswordRetrieval: "启用自助找回密码", enableCommonSelfServicePasswordRetrieval: "启用自助找回密码",
saveButton: "保存", saveButton: "保存",
stopSuccess: "停止成功", stopSuccess: "停止成功",

View File

@ -1,7 +1,8 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import * as api from "./api.plugin"; import * as api from "./api.plugin";
import { DynamicType, FormItemProps } from "@fast-crud/fast-crud"; import { DynamicType, FormItemProps, useMerge } from "@fast-crud/fast-crud";
import { i18n } from "/src/locales/i18n"; import { i18n } from "/src/locales/i18n";
import { cloneDeep } from "lodash-es";
interface PluginState { interface PluginState {
group?: PluginGroups; group?: PluginGroups;
} }
@ -32,14 +33,17 @@ export class PluginGroups {
groups!: { [key: string]: PluginGroup }; groups!: { [key: string]: PluginGroup };
map!: { [key: string]: PluginDefine }; map!: { [key: string]: PluginDefine };
t: any; t: any;
constructor(groups: { [key: string]: PluginGroup }) { mergeSetting?: boolean;
constructor(groups: { [key: string]: PluginGroup }, opts?: { mergeSetting?: boolean }) {
this.groups = groups; this.groups = groups;
this.t = i18n.global.t; this.t = i18n.global.t;
this.mergeSetting = opts?.mergeSetting ?? false;
this.initGroup(groups); this.initGroup(groups);
this.initMap(); this.initMap();
} }
private initGroup(groups: { [p: string]: PluginGroup }) { private initGroup(groups: { [p: string]: PluginGroup }) {
const { merge } = useMerge();
const all: PluginGroup = { const all: PluginGroup = {
key: "all", key: "all",
title: this.t("certd.all"), title: this.t("certd.all"),
@ -48,6 +52,14 @@ export class PluginGroups {
icon: "material-symbols:border-all-rounded", icon: "material-symbols:border-all-rounded",
}; };
for (const key in groups) { for (const key in groups) {
if (this.mergeSetting) {
for (const plugin of groups[key].plugins) {
if (plugin.sysSetting) {
merge(plugin.input, plugin.sysSetting.metadata);
}
}
}
all.plugins.push(...groups[key].plugins); all.plugins.push(...groups[key].plugins);
} }
this.groups = { this.groups = {
@ -132,11 +144,15 @@ export const usePluginStore = defineStore({
id: "app.plugin", id: "app.plugin",
state: (): PluginState => ({ state: (): PluginState => ({
group: null, group: null,
originGroup: null,
}), }),
actions: { actions: {
async reload() { async reload() {
const groups = await api.GetGroups({}); const groups = await api.GetGroups({});
this.group = new PluginGroups(groups); this.group = new PluginGroups(groups, { mergeSetting: true });
this.originGroup = new PluginGroups(cloneDeep(groups));
console.log("group", this.group);
console.log("originGroup", this.originGroup);
}, },
async init() { async init() {
if (!this.group) { if (!this.group) {
@ -159,6 +175,10 @@ export const usePluginStore = defineStore({
await this.init(); await this.init();
return this.group.get(name); return this.group.get(name);
}, },
async getPluginDefineFromOrigin(name: string): Promise<PluginDefine> {
await this.init();
return this.originGroup.get(name);
},
async getPluginConfig(query: any) { async getPluginConfig(query: any) {
return await api.GetPluginConfig(query); return await api.GetPluginConfig(query);
}, },

View File

@ -304,3 +304,11 @@ h6 {
padding: 10px; padding: 10px;
color: #6e6e6e; color: #6e6e6e;
} }
.ant-modal-body{
.fs-form-body{
max-height: 66vh;
overflow-y: auto;
}
}

View File

@ -138,6 +138,7 @@ export function useCertPipelineCreator() {
form: { form: {
doSubmit, doSubmit,
wrapper: { wrapper: {
wrapClassName: "cert_pipeline_create_form",
width: 1350, width: 1350,
saveRemind: false, saveRemind: false,
title: t("certd.pipelineForm.createTitle"), title: t("certd.pipelineForm.createTitle"),

View File

@ -115,4 +115,13 @@ function batchRerun() {
padding-left: 10px; padding-left: 10px;
} }
} }
.cert_pipeline_create_form {
.ant-collapse {
margin: 10px;
}
.ant-collapse-header {
text-align: right;
}
}
</style> </style>

View File

@ -43,7 +43,7 @@
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
<a-form-item> <a-form-item>
<a-button type="primary" size="large" html-type="submit" :loading="loading" class="login-button"> <a-button type="primary" size="large" html-type="button" :loading="loading" class="login-button" @click="handleFinish">
{{ t("authentication.loginButton") }} {{ t("authentication.loginButton") }}
</a-button> </a-button>
@ -217,7 +217,6 @@ export default defineComponent({
</script> </script>
<style lang="less"> <style lang="less">
.login-page.main { .login-page.main {
//margin: 20px !important; //margin: 20px !important;
margin-bottom: 100px; margin-bottom: 100px;

View File

@ -7,38 +7,49 @@
</div> </div>
<div class="p-10"> <div class="p-10">
<div ref="formRef" class="config-form w-full" :label-col="labelCol" :wrapper-col="wrapperCol"> <div ref="formRef" class="config-form w-full" :label-col="labelCol" :wrapper-col="wrapperCol">
<template v-for="(item, key) in originInputs" :key="key"> <table class="table-fixed w-full">
<div> <thead>
<div :label="item.title"> <tr>
<label class="flex mt-5"> <th class="text-left p-5" width="200px">插件参数</th>
<span class="w-20 flex-shrink-0">默认值</span> <th class="text-left p-5" width="100px">参数配置</th>
<rollbackable :value="configForm[key].value" @set="configForm[key].value = item.value ?? null" @clear="unset(configForm, `${key}.value`)"></rollbackable> <th class="text-left flex-1 p-5">自定义</th>
</label> </tr>
<label class="flex mt-5"> </thead>
<span class="w-20 flex-shrink-0">是否显示</span> <tbody>
<rollbackable :value="configForm[key].show" @set="set(configForm, `${key}.show`, item.show ?? null)" @clear="unset(configForm, `${key}.show`)"></rollbackable> <template v-for="(item, key) in originInputs" :key="key">
</label> <template v-for="prop in editableKeys" :key="prop.key">
<label class="flex mt-5"> <tr>
<span class="w-20 flex-shrink-0">帮助说明</span> <td v-if="prop.key === 'value'" class="border-t-2 p-5" rowspan="3" :class="{ 'border-t-2': prop.key === 'value' }">{{ item.title }}</td>
{{ configForm[key].helper }} <td class="border-t p-5" :class="{ 'border-t-2': prop.key === 'value' }">{{ prop.label }}</td>
<rollbackable :value="configForm[key].helper" @setter="configForm[key].helper = item.helper" @clear1="delete configForm[key].helper"></rollbackable> <td class="border-t p-5" :class="{ 'border-t-2': prop.key === 'value' }">
</label> <rollbackable :value="configForm[key][prop.key]" @set="configForm[key][prop.key] = item[prop.key] ?? null" @clear="delete configForm[key][prop.key]">
</div> <template #default>
</div> <fs-render :render-func="prop.defaultRender(key, item)"></fs-render>
</template> </template>
<template #edit>
<fs-render :render-func="prop.editRender(key, item)"></fs-render>
</template>
</rollbackable>
</td>
</tr>
</template>
</template>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="tsx">
import { computed, nextTick, onMounted, reactive, ref, Ref, unref } from "vue"; import { computed, nextTick, onMounted, reactive, ref, Ref, unref } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import * as api from "./api"; import * as api from "./api";
import { usePluginStore } from "/@/store/plugin"; import { usePluginStore } from "/@/store/plugin";
import { cloneDeep, get, merge, set, unset } from "lodash-es"; import { cloneDeep, get, merge, set, unset } from "lodash-es";
import Rollbackable from "./rollbackable.vue"; import Rollbackable from "./rollbackable.vue";
import { FsRender } from "@fast-crud/fast-crud";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const pluginStore = usePluginStore(); const pluginStore = usePluginStore();
@ -48,7 +59,6 @@ const props = defineProps<{
const pluginMetadata = ref<any>(""); const pluginMetadata = ref<any>("");
const currentPlugin = ref(); const currentPlugin = ref();
console.log("111111111111111111111");
const labelCol = ref({ const labelCol = ref({
span: null, span: null,
style: { style: {
@ -71,9 +81,49 @@ function getForm() {
} }
const editableKeys = ref([ const editableKeys = ref([
{ key: "value", label: "默认值" }, {
{ key: "show", label: "是否显示", component: { name: "a-switch", vModel: "checked" } }, key: "value",
{ key: "helper", label: "帮助说明", component: { name: "a-textarea", vModel: "value", rows: 4 } }, label: "默认值",
defaultRender(key: string, item: any) {
return () => {
return item["value"] ?? "";
};
},
editRender(key: string, item: any) {
return () => {
return <fs-component-render {...item.component} vModel:modelValue={configForm[key]["value"]} scope={getScope()} />;
};
},
},
{
key: "show",
label: "是否显示",
defaultRender(key: string, item: any) {
return () => {
const value = item["show"];
return value === false ? "不显示" : "显示";
};
},
editRender(key: string, item: any) {
return () => {
return <a-switch vModel:checked={configForm[key]["show"]} />;
};
},
},
{
key: "helper",
label: "帮助说明",
defaultRender(key: string, item: any) {
return () => {
return <pre class={"helper"}>{item["helper"]}</pre>;
};
},
editRender(key: string, item: any) {
return () => {
return <a-textarea rows={5} vModel:value={configForm[key]["helper"]} />;
};
},
},
]); ]);
const originInputs = computed(() => { const originInputs = computed(() => {
@ -98,7 +148,7 @@ function clearFormValue(key: string) {
} }
async function loadPluginSetting() { async function loadPluginSetting() {
currentPlugin.value = await pluginStore.getPluginDefine(props.plugin.name); currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(props.plugin.name);
for (const key in currentPlugin.value.input) { for (const key in currentPlugin.value.input) {
configForm[key] = {}; configForm[key] = {};
} }
@ -113,7 +163,9 @@ onMounted(async () => {
await loadPluginSetting(); await loadPluginSetting();
}); });
function doSave() {} defineExpose({
getForm,
});
</script> </script>
<style lang="less"> <style lang="less">

View File

@ -1,32 +1,45 @@
<script setup lang="ts"> <script setup lang="ts">
import { cloneDeep, get, merge, set, unset } from "lodash-es";
import { defineProps } from "vue"; import { defineProps } from "vue";
const props = defineProps<{ value: any }>(); const props = defineProps<{ value: any }>();
const emits = defineEmits(["setter", "clear1"]); const emits = defineEmits(["set", "clear"]);
function setValue() { function setValue() {
console.log("33333"); emits("set");
emits("setter");
} }
function clearValue() { function clearValue() {
console.log("4444"); emits("clear");
emits("clear1");
} }
</script> </script>
<template> <template>
<div class="rollbackable"> <div class="rollbackable">
<div class="flex"> <div class="flex">
<div> <div style="width: 100px">
<a-tag v-if="value === undefined" color="green" size="small" class="pointer flex-inline items-center" @click.stop="setValue">
<fs-icon icon="material-symbols:edit" class="mr-5"></fs-icon>
自定义
</a-tag>
<a-tag v-else color="red" size="small" class="pointer flex-inline items-center" @click.stop="clearValue">
<fs-icon icon="material-symbols:undo" class="mr-5"></fs-icon>
还原
</a-tag>
</div>
<div class="flex-1 overflow-hidden value-render">
<slot v-if="value === undefined" name="default"></slot> <slot v-if="value === undefined" name="default"></slot>
<slot v-else name="edit"></slot> <slot v-else name="edit"></slot>
</div> </div>
<div>
<div v-if="value === undefined" type="primary" size="small" @click.stop="setValue"></div>
<div v-else style="margin-left: 100px" type="primary" size="small" @click.stop="clearValue">还原</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="less"></style> <style lang="less">
.rollbackable {
.value-render {
.ant-select,
.ant-input {
width: 100%;
}
}
}
</style>

View File

@ -4,54 +4,67 @@ import { useI18n } from "/@/locales";
import { Modal, notification } from "ant-design-vue"; import { Modal, notification } from "ant-design-vue";
import ConfigEditor from "./config-editor.vue"; import ConfigEditor from "./config-editor.vue";
import { useModal } from "/@/use/use-modal"; import { useModal } from "/@/use/use-modal";
import { ref } from "vue";
export function usePluginConfig() { export function usePluginConfig() {
const { openCrudFormDialog } = useFormWrapper(); const { openCrudFormDialog } = useFormWrapper();
const { t } = useI18n(); const { t } = useI18n();
const modal = useModal(); const modal = useModal();
async function openConfigDialog({ row, crudExpose }) { async function openConfigDialog({ row, crudExpose }) {
// function createCrudOptions() { const configEditorRef = ref();
// return { function createCrudOptions() {
// crudOptions: { return {
// columns: {}, crudOptions: {
// form: { columns: {},
// wrapper: { form: {
// width: "80%", wrapper: {
// title: "插件元数据配置", width: "80%",
// saveRemind: false, title: "插件元数据配置",
// slots: { saveRemind: false,
// "form-body-top": () => { slots: {
// return ( "form-body-top": () => {
// <div> return (
// <ConfigEditor plugin={row}></ConfigEditor> <div>
// </div> <ConfigEditor ref={configEditorRef} plugin={row}></ConfigEditor>
// ); </div>
// }, );
// }, },
// }, },
// afterSubmit() { },
// notification.success({ message: t("certd.operationSuccess") }); afterSubmit() {
// crudExpose.doRefresh(); notification.success({ message: t("certd.operationSuccess") });
// }, crudExpose.doRefresh();
// async doSubmit({ form }: any) { },
// return await api.ImportPlugin({ async doSubmit({}: any) {
// ...form, const form = configEditorRef.value.getForm();
// }); const newForm: any = {};
// }, for (const key in form) {
// }, const value = form[key];
// }, if (value && Object.keys(value).length > 0) {
// }; newForm[key] = value;
// } }
// const { crudOptions } = createCrudOptions(); }
// await openCrudFormDialog({ crudOptions }); return await api.savePluginSetting({
name: row.name,
sysSetting: {
metadata: newForm,
},
});
},
},
},
};
}
const { crudOptions } = createCrudOptions();
await openCrudFormDialog({ crudOptions });
modal.confirm({ // modal.confirm({
title: "插件元数据配置", // title: "插件元数据配置",
width: "80%", // width: "80%",
content: () => { // content: () => {
return <ConfigEditor plugin={row}></ConfigEditor>; // return <ConfigEditor plugin={row}></ConfigEditor>;
}, // },
}); // });
} }
return { return {

View File

@ -2,7 +2,11 @@ import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/c
import { merge } from 'lodash-es'; import { merge } from 'lodash-es';
import { CrudController } from '@certd/lib-server'; import { CrudController } from '@certd/lib-server';
import { PluginImportReq, 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,
PluginConfig,
PluginConfigService
} from '../../../modules/plugin/service/plugin-config-service.js';
/** /**
* *
*/ */
@ -79,7 +83,11 @@ 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('/saveSetting', { summary: 'sys:settings:edit' })
async saveSetting(@Body(ALL) body: PluginConfig) {
const res = await this.pluginConfigService.savePluginConfig(body);
return this.ok(res);
}
@Post('/import', { summary: 'sys:settings:edit' }) @Post('/import', { summary: 'sys:settings:edit' })
async import(@Body(ALL) body: PluginImportReq) { async import(@Body(ALL) body: PluginImportReq) {

View File

@ -3,7 +3,7 @@ import { PluginService } from './plugin-service.js';
export type PluginConfig = { export type PluginConfig = {
name: string; name: string;
disabled: boolean; disabled?: boolean;
sysSetting: { sysSetting: {
input?: Record<string, any>; input?: Record<string, any>;
metadata?: Record<string, any>; metadata?: Record<string, any>;
@ -38,10 +38,12 @@ export class PluginConfigService {
} }
async saveCommPluginConfig(config: CommPluginConfig) { async saveCommPluginConfig(config: CommPluginConfig) {
await this.savePluginConfig('CertApply', config.CertApply); config.CertApply.name = 'CertApply';
await this.savePluginConfig(config.CertApply);
} }
async savePluginConfig(name: string, config: PluginConfig) { async savePluginConfig( config: PluginConfig) {
const name = config.name;
const sysSetting = config?.sysSetting; const sysSetting = config?.sysSetting;
if (!sysSetting) { if (!sysSetting) {
throw new Error(`${name}.sysSetting is required`); throw new Error(`${name}.sysSetting is required`);

View File

@ -1,16 +1,16 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import {Inject, Provide, Scope, ScopeEnum} from "@midwayjs/core";
import { BaseService, PageReq } from "@certd/lib-server"; import {BaseService, PageReq} from "@certd/lib-server";
import { PluginEntity } from "../entity/plugin.js"; import {PluginEntity} from "../entity/plugin.js";
import { InjectEntityModel } from "@midwayjs/typeorm"; import {InjectEntityModel} from "@midwayjs/typeorm";
import { Repository } from "typeorm"; import {IsNull, Not, Repository} from "typeorm";
import { isComm } from "@certd/plus-core"; import {isComm} from "@certd/plus-core";
import { BuiltInPluginService } from "../../pipeline/service/builtin-plugin-service.js"; import {BuiltInPluginService} from "../../pipeline/service/builtin-plugin-service.js";
import { merge } from "lodash-es"; import {merge} from "lodash-es";
import { accessRegistry, notificationRegistry, pluginRegistry } from "@certd/pipeline"; import {accessRegistry, notificationRegistry, pluginRegistry} from "@certd/pipeline";
import { dnsProviderRegistry } from "@certd/plugin-cert"; import {dnsProviderRegistry} from "@certd/plugin-cert";
import { logger } from "@certd/basic"; 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";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
@ -57,9 +57,9 @@ export class PluginService extends BaseService<PluginEntity> {
}; };
} }
async getEnabledBuildInGroup(isSimple = false) { async getEnabledBuildInGroup(opts?:{isSimple?:boolean,withSetting?:boolean}) {
const groups = this.builtInPluginService.getGroups(); const groups = this.builtInPluginService.getGroups();
if (isSimple) { if (opts?.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 => {
@ -72,9 +72,43 @@ export class PluginService extends BaseService<PluginEntity> {
if (!isComm()) { if (!isComm()) {
return groups; return groups;
} }
// 初始化设置
const settingPlugins = await this.repository.find({
select:{
id:true,
name:true,
sysSetting:true
},
where: {
sysSetting : Not(IsNull())
}
})
//合并插件配置
const pluginSettingMap:any = {}
for (const item of settingPlugins) {
if (!item.sysSetting) {
continue;
}
pluginSettingMap[item.name] = JSON.parse(item.sysSetting);
}
for (const key in groups) {
const group = groups[key];
if (!group.plugins) {
continue;
}
for (const item of group.plugins) {
const pluginSetting = pluginSettingMap[item.name];
if (pluginSetting){
item.sysSetting = pluginSetting
}
}
}
//排除禁用的
const list = await this.list({ const list = await this.list({
query: { query: {
type: "builtIn",
disabled: true disabled: true
} }
}); });