chore: plugin default

pull/370/head
xiaojunnuo 2025-04-10 00:22:05 +08:00
parent 7545194f97
commit 0e36f03954
5 changed files with 336 additions and 25 deletions

View File

@ -77,7 +77,7 @@ onUnmounted(() => {
disposeEditor(); disposeEditor();
}); });
const emits = defineEmits(["update:modelValue", "change", "ready"]); const emits = defineEmits(["update:modelValue", "change", "ready", "save"]);
const emitValue = lodashDebounce((value: any) => { const emitValue = lodashDebounce((value: any) => {
emits("update:modelValue", value); emits("update:modelValue", value);
@ -91,6 +91,10 @@ async function createEditor(ctx: EditorCodeCtx) {
language: ctx.language, language: ctx.language,
theme: "vs-dark", theme: "vs-dark",
minimap: { enabled: false }, minimap: { enabled: false },
insertSpaces: true,
tabSize: 2,
autoIndent: true, //
renderWhitespace: true,
readOnly: props.readonly || props.disabled, readOnly: props.readonly || props.disabled,
hover: { hover: {
enabled: true, enabled: true,
@ -113,6 +117,20 @@ async function createEditor(ctx: EditorCodeCtx) {
if (props.modelValue) { if (props.modelValue) {
instanceRef.setValue(props.modelValue); instanceRef.setValue(props.modelValue);
} }
instance.addAction({
id: "custom_save",
label: "save",
contextMenuOrder: 1, //
contextMenuGroupId: "customCommand", //
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
run: async () => {
await instanceRef.getAction("editor.action.formatDocument").run();
emits("save", {
value: props.modelValue,
});
}, //
});
return instance; return instance;
} }
@ -128,7 +146,7 @@ async function initJson(ctx: EditorCodeCtx) {
const schemas = []; const schemas = [];
if (ctx.schema) { if (ctx.schema) {
schemas.push({ schemas.push({
// uri: "http://myserver/foo-schema.json", // id of the first schema uri: "http://myserver/foo-schema.json", // id of the first schema
fileMatch: ["*"], // associate with our model fileMatch: ["*"], // associate with our model
schema: { schema: {
...ctx.schema, ...ctx.schema,
@ -147,7 +165,8 @@ async function initYaml(ctx: EditorCodeCtx) {
await importYamlContribution(); await importYamlContribution();
const { configureMonacoYaml } = await importMonacoYaml(); const { configureMonacoYaml } = await importMonacoYaml();
monaco.languages.register({ id: "yaml" }); monaco.languages.register({ id: "yaml" });
const id = `fs-editor-code-yaml-${props.id || ""}.yaml`;
const uri = monaco.Uri.parse(id);
const schemas = []; const schemas = [];
if (ctx.schema) { if (ctx.schema) {
schemas.push({ schemas.push({
@ -155,7 +174,7 @@ async function initYaml(ctx: EditorCodeCtx) {
schema: { schema: {
...ctx.schema, ...ctx.schema,
}, },
uri: "http://myserver/foo-schema.json", uri: id,
}); });
} }
configureMonacoYaml(monaco, { configureMonacoYaml(monaco, {
@ -167,7 +186,7 @@ async function initYaml(ctx: EditorCodeCtx) {
isKubernetes: false, isKubernetes: false,
enableSchemaRequest: false, enableSchemaRequest: false,
}); });
const uri = monaco.Uri.parse(`fs-editor-code-yaml-${props.id || ""}.yaml`);
const oldModel = monaco.editor.getModel(uri); const oldModel = monaco.editor.getModel(uri);
if (oldModel) { if (oldModel) {
oldModel.dispose(); oldModel.dispose();

View File

@ -117,7 +117,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
form: { form: {
show: true, show: true,
order: 0, order: 0,
helper: "必须为英文,驼峰命名,类型作为前缀\n例如AliyunDeployToCDN", helper: "必须为英文,驼峰命名,类型作为前缀\n例如AliyunDeployToCDN\n插件一旦被使用不要修改名称",
rules: [{ required: true }], rules: [{ required: true }],
}, },
column: { column: {
@ -140,7 +140,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
form: { form: {
show: true, show: true,
order: 0, order: 0,
helper: "上传到应用商店时,将作为插件名称前缀,例如greper/pluginName", helper: "上传到插件商店时,将作为插件名称前缀,例如greper/pluginName",
rules: [{ required: true }], rules: [{ required: true }],
}, },
column: { column: {
@ -187,7 +187,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
desc: { desc: {
title: "描述", title: "描述",
type: "text", type: "textarea",
helper: "插件的描述", helper: "插件的描述",
column: { column: {
width: 300, width: 300,
@ -230,6 +230,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
form: { form: {
rules: [{ required: true }], rules: [{ required: true }],
}, },
editForm: {
component: {
disabled: true,
},
},
dict: dict({ dict: dict({
data: [ data: [
{ label: "授权", value: "access" }, { label: "授权", value: "access" },
@ -245,6 +250,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
}, },
version: {
title: "版本",
type: "text",
column: {
width: 100,
align: "center",
},
},
group: { group: {
title: "分组", title: "分组",
type: "dict-select", type: "dict-select",
@ -264,6 +277,29 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
}, },
"default.strategy.runStrategy": {
title: "运行策略",
type: "dict-select",
dict: dict({
data: [
{ value: 0, label: "正常运行" },
{ value: 1, label: "成功后跳过(部署任务)" },
],
}),
form: {
value: 1,
rules: [{ required: true }],
helper: "默认运行策略",
},
column: {
width: 100,
align: "left",
component: {
color: "auto",
},
},
},
disabled: { disabled: {
title: "点击禁用/启用", title: "点击禁用/启用",
type: "dict-switch", type: "dict-switch",
@ -274,6 +310,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
], ],
}), }),
form: { form: {
title: "禁用/启用",
value: false, value: false,
}, },
column: { column: {

View File

@ -13,29 +13,39 @@
</div> </div>
</template> </template>
<div class="pi-plugin-editor"> <div class="pi-plugin-editor">
<div class="base">
<a-tabs type="card">
<a-tab-pane key="base" tab="插件信息"> </a-tab-pane>
</a-tabs>
<div class="base-body">
<fs-form ref="baseFormRef" v-bind="formOptionsRef"></fs-form>
</div>
</div>
<div class="metadata"> <div class="metadata">
<a-tabs type="card"> <a-tabs type="card">
<a-tab-pane key="editor" tab="元数据"> </a-tab-pane> <a-tab-pane key="editor" tab="元数据"> </a-tab-pane>
</a-tabs> </a-tabs>
<div class="metadata-body"> <div class="metadata-body">
<code-editor id="metadata" v-model:model-value="plugin.metadata" language="yaml"></code-editor> <code-editor id="metadata" v-model:model-value="plugin.metadata" language="yaml" @save="doSave"></code-editor>
</div> </div>
</div> </div>
<div class="script"> <div class="script">
<a-tabs type="card"> <a-tabs type="card">
<a-tab-pane key="script" tab="脚本"> </a-tab-pane> <a-tab-pane key="script" tab="脚本"> </a-tab-pane>
</a-tabs> </a-tabs>
<code-editor id="content" v-model:model-value="plugin.content" language="javascript"></code-editor> <code-editor id="content" v-model:model-value="plugin.content" language="javascript" @save="doSave"></code-editor>
</div> </div>
</div> </div>
</fs-page> </fs-page>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, provide, ref } from "vue"; import { onMounted, provide, ref, Ref } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import * as api from "./api"; import * as api from "./api";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { notification } from "ant-design-vue"; import { notification } from "ant-design-vue";
import createCrudOptions from "./crud";
import { useColumns } from "@fast-crud/fast-crud";
const CertApplyPluginNames = ["CertApply", "CertApplyLego", "CertApplyUpload"]; const CertApplyPluginNames = ["CertApply", "CertApplyLego", "CertApplyUpload"];
defineOptions({ defineOptions({
@ -44,25 +54,49 @@ defineOptions({
const route = useRoute(); const route = useRoute();
const plugin = ref<any>({}); const plugin = ref<any>({});
const formOptionsRef: Ref = ref();
const baseFormRef: Ref = ref({});
function initFormOptions() {
const formCrudOptions = createCrudOptions({
crudExpose: {},
context: {},
});
const { buildFormOptions } = useColumns();
const formOptions = buildFormOptions(formCrudOptions.crudOptions, {});
formOptions.col = {
span: 24,
};
formOptions.labelCol = {
style: {
width: "100px",
},
};
formOptionsRef.value = formOptions;
}
initFormOptions();
async function getPlugin() { async function getPlugin() {
const id = route.query.id; const id = route.query.id;
const pluginObj = await api.GetObj(id); const pluginObj = await api.GetObj(id);
if (!pluginObj.metadata) { if (pluginObj.metadata) {
pluginObj.metadata = yaml.dump({ const metadata = yaml.load(pluginObj.metadata);
input: { pluginObj.default = metadata.default || {};
cert: { delete metadata.default;
title: "前置任务生成的证书", pluginObj.metadata = yaml.dump(metadata, {
component: { indent: 2,
name: "output-selector",
from: [...CertApplyPluginNames],
},
},
},
output: {},
}); });
} }
plugin.value = pluginObj; plugin.value = pluginObj;
const baseFrom = {
...pluginObj,
};
delete baseFrom.metadata;
delete baseFrom.content;
baseFormRef.value.setFormData(baseFrom);
} }
onMounted(async () => { onMounted(async () => {
@ -76,8 +110,16 @@ provide("get:plugin", () => {
const saveLoading = ref(false); const saveLoading = ref(false);
async function doSave() { async function doSave() {
saveLoading.value = true; saveLoading.value = true;
const baseForm = baseFormRef.value.getFormData();
const metadata = yaml.load(plugin.value.metadata);
metadata.default = baseForm.default;
const form = {
...plugin.value,
...baseForm,
metadata: yaml.dump(metadata, { indent: 2 }),
};
try { try {
await api.UpdateObj(plugin.value); await api.UpdateObj(form);
notification.success({ notification.success({
message: "保存成功", message: "保存成功",
}); });
@ -112,9 +154,18 @@ async function doTest() {
flex: 1; flex: 1;
} }
.base {
width: 400px;
max-width: 30%;
margin-right: 20px;
display: flex;
flex-direction: column;
height: 100%;
}
.metadata { .metadata {
width: 600px; width: 600px;
max-width: 50%; max-width: 30%;
margin-right: 20px; margin-right: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -0,0 +1,169 @@
import yaml from "js-yaml";
const CertOutputs = [
"CertApply",
"CertApplyLego",
"CertApplyUpload"
];
export function getDefaultAccessPlugin() {
const metadata = {
username: {
title: "用户名",
required: true,
encrypt: false
},
password: {
title: "密码",
required: true,
encrypt: true
}
};
const script = `const { BaseAccess } = await import("@certd/pipeline")
return class DemoAccess extends BaseAccess {
username;
password;
}
`;
return {
metadata:yaml.dump(metadata),
content: script
};
}
export function getDefaultDeployPlugin() {
const metadata = {
cert: {
title: "前置任务证书",
component: {
name: "output-selector",
from: [...CertOutputs]
},
required: true
},
certDomains: {
title: "当前证书域名",
component: {
name: "cert-domains-getter"
},
mergeScript: `
return {
component:{
inputKey: ctx.compute(({form})=>{
return form.cert
}),
}
}
`,
required: true
},
accessId: {
title: "Access授权",
helper: "xxxx的授权",
component: {
name: "access-selector",
type: "aliyun"
},
required: true
},
key1: {
title: "输入示例1",
required: false
},
key2: {
title: "可选项",
component: {
name: "a-select",
vMode: "value",
options: [
{ value: "1", label: "选项1" },
{ value: "2", label: "选项2" }
]
},
required: false
}
};
const script = `
const { AbstractTaskPlugin } = await import("@certd/pipeline")
return class DemoTask extends AbstractTaskPlugin {
cert;
certDomains;
accessId;
key1;
key2;
async execute(){
const access = await this.accessService.getById(this.accessId)
this.logger.info("cert:",this.cert);
this.logger.info("certDomains:",this.certDomains);
this.logger.info("access:",access);
this.logger.info("key1:",this.key1);
this.logger.info("key2:",this.key2);
//开始你的部署任务
const res = await this.ctx.http.request({url:"xxxxxx"})
if(res.error){
//抛出异常,终止任务,否则将被判定为执行成功
throw new Error("部署失败:"+res.message)
}
//必须使用this.logger打印日志
this.logger.info("执行成功")
}
}
`
return {
metadata: yaml.dump(metadata),
content: script
};
}
export function getDefaultDnsPlugin() {
const metadata = `accessType: aliyun #授权类型名称`
const script = `
const { AbstractDnsProvider } = await import("@certd/pipeline")
return class DemoDnsProvider extends AbstractDnsProvider {
// 创建dns解析记录用于验证域名所有权
async createRecord(options) {
/**
* fullRecord: '_acme-challenge.test.example.com',
* value: uuid
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, value, type, domain } = options;
const access = this.ctx.access
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
// const record = await sdk.createRecord() // 调用对应的接口创建解析记录
//返回解析记录,用于后面清理
return record
}
/**
* dns,
* @param options
*/
async removeRecord(options) {
const { fullRecord, value } = options.recordReq;
const record = options.recordRes; // createRecord接口返回的record
const access = this.ctx.access
this.logger.info('删除域名解析:', fullRecord, value);
if (!record) {
this.logger.info('record为空不执行删除');
return;
}
const recordId = record.id;
// 这里调用删除txt dns解析记录接口
// sdk.removeRecord(recordId)
this.logger.info("删除域名解析成功");
}
}
`
return {
metadata: metadata,
content: script
}
}

View File

@ -10,6 +10,7 @@ import { accessRegistry, 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";
@Provide() @Provide()
@ -143,6 +144,40 @@ export class PluginService extends BaseService<PluginEntity> {
return this.builtInPluginService.getByType(type); 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()
}else if (param.pluginType === "deploy") {
plugin = getDefaultDeployPlugin()
}else if (param.pluginType === "dnsProvider") {
plugin = getDefaultDnsPlugin()
}else{
throw new Error(`插件类型${param.pluginType}不支持`);
}
return await super.add({
...param,
...plugin
});
}
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, {