perf: 部署支持1Panel

pull/199/head
xiaojunnuo 2024-09-27 02:15:41 +08:00
parent 3f21a49988
commit d047234d98
20 changed files with 342 additions and 95 deletions

View File

@ -34,7 +34,7 @@
"mocha": "^10.6.0",
"nock": "^13.5.4",
"tsd": "^0.31.1",
"typescript": "^4.8.4",
"typescript": "^5.4.2",
"uuid": "^8.3.2"
},
"scripts": {

View File

@ -21,6 +21,7 @@
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.5",
"lodash-es": "^4.17.21",
"lru-cache": "^10.0.0",
"node-forge": "^1.3.1",
"nodemailer": "^6.9.3",
"proxy-agent": "^6.4.0",
@ -58,7 +59,7 @@
"ts-node": "^10.9.1",
"tsc-esm-fix": "^3.0.0",
"tslib": "^2.5.2",
"typescript": "^5.0.4",
"typescript": "^5.4.2",
"vite": "^4.3.8",
"vue-tsc": "^1.6.5"
},

View File

@ -1,21 +1,28 @@
import _ from "lodash-es";
import { HttpClient, ILogger } from "../utils";
import { HttpClient, ILogger, utils } from "../utils";
export type PluginRequest = {
type: "plugin" | "access";
export type PluginRequestHandleReq<T = any> = {
typeName: string;
action: string;
input: any;
input: T;
data: any;
};
export type RequestHandleContext = {
export type AccessRequestHandleReqInput<T = any> = {
id?: number;
title?: string;
access: T;
};
export type AccessRequestHandleReq<T = any> = PluginRequestHandleReq<AccessRequestHandleReqInput<T>>;
export type AccessRequestHandleContext = {
http: HttpClient;
logger: ILogger;
utils: typeof utils;
};
export class RequestHandler {
async onRequest(req: PluginRequest, ctx: RequestHandleContext) {
export class AccessRequestHandler<T = any> {
async onRequest(req: AccessRequestHandleReq<T>, ctx: AccessRequestHandleContext) {
if (!req.action) {
throw new Error("action is required");
}
@ -31,3 +38,4 @@ export class RequestHandler {
throw new Error(`action ${req.action} not found`);
}
}

View File

@ -4,11 +4,12 @@ import { FileStore } from "../core/file-store.js";
import { Logger } from "log4js";
import { IAccessService } from "../access/index.js";
import { IEmailService } from "../service/index.js";
import { IContext } from "../core/index.js";
import { ILogger, logger } from "../utils/index.js";
import { IContext, PluginRequestHandleReq } from "../core/index.js";
import { ILogger, logger, utils } from "../utils/index.js";
import { HttpClient } from "../utils/util.request";
import { utils } from "../utils/index.js";
import dayjs from "dayjs";
import _ from "lodash-es";
export enum ContextScope {
global,
pipeline,
@ -38,18 +39,13 @@ export type PluginDefine = Registrable & {
[key: string]: any;
};
reference?: {
src: string;
dest: string;
type: "computed";
}[];
needPlus?: boolean;
};
export type ITaskPlugin = {
onInstance(): Promise<void>;
execute(): Promise<void>;
onRequest(req: PluginRequestHandleReq<any>): Promise<any>;
[key: string]: any;
};
@ -107,6 +103,17 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
this.accessService = ctx.accessService;
}
async getAccess(accessId: string) {
if (accessId == null) {
throw new Error("您还没有配置授权");
}
const res = await this.ctx.accessService.getById(accessId);
if (res == null) {
throw new Error("授权不存在,可能已被删除,请前往任务配置里面重新选择授权");
}
return res;
}
randomFileId() {
return Math.random().toString(36).substring(2, 9);
}
@ -147,6 +154,22 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
}
return name + "_" + dayjs().format("YYYYMMDDHHmmss");
}
async onRequest(req: PluginRequestHandleReq<any>) {
if (!req.action) {
throw new Error("action is required");
}
const methodName = `on${_.upperFirst(req.action)}`;
// @ts-ignore
const method = this[methodName];
if (method) {
// @ts-ignore
return await this[methodName](req.data);
}
throw new Error(`action ${req.action} not found`);
}
}
export type OutputVO = {

View File

@ -22,5 +22,6 @@ export const pluginGroups = {
tencent: new PluginGroup("tencent", "腾讯云", 4),
host: new PluginGroup("host", "主机", 5),
cdn: new PluginGroup("cdn", "CDN", 6),
other: new PluginGroup("other", "其他", 7),
panel: new PluginGroup("panel", "面板", 7),
other: new PluginGroup("other", "其他", 8),
};

View File

@ -6,11 +6,15 @@ export * from "./util.file.js";
export * from "./util.sp.js";
export * from "./util.promise.js";
export * from "./util.hash.js";
export * from "./util.merge.js";
export * from "./util.cache.js";
import { mergeUtils } from "./util.merge.js";
import { sp } from "./util.sp.js";
import { hashUtils } from "./util.hash.js";
import { promises } from "./util.promise.js";
import { fileUtils } from "./util.file.js";
import _ from "lodash-es";
import { cache } from "./util.cache.js";
export const utils = {
sleep,
http,
@ -19,4 +23,6 @@ export const utils = {
promises,
file: fileUtils,
_,
mergeUtils,
cache,
};

View File

@ -0,0 +1,8 @@
// LRUCache
import { LRUCache } from "lru-cache";
export const cache = new LRUCache<string, any>({
max: 1000,
ttl: 1000 * 60 * 10,
});

View File

@ -0,0 +1,64 @@
import _ from "lodash-es";
function isUnMergeable(srcValue: any) {
return srcValue != null && srcValue instanceof UnMergeable;
}
function isUnCloneable(value: any) {
return isUnMergeable(value) && !value.cloneable;
}
function merge(target: any, ...sources: any) {
/**
* arrayunMergeableref,
* @param objValue
* @param srcValue
*/
function customizer(objValue: any, srcValue: any) {
if (srcValue == null) {
return;
}
// 如果被合并对象为数组,则直接被覆盖对象覆盖,只要覆盖对象不为空
if (_.isArray(objValue)) {
//原对象如果是数组
return srcValue; //来源对象
}
if (isUnMergeable(srcValue)) {
return srcValue;
}
}
let found: any = null;
for (const item of sources) {
if (isUnMergeable(item)) {
found = item;
}
}
if (found) {
return found;
}
return _.mergeWith(target, ...sources, customizer);
}
function cloneDeep(target: any) {
if (isUnCloneable(target)) {
return target;
}
function customizer(value: any) {
if (isUnCloneable(value)) {
return value;
}
}
return _.cloneDeepWith(target, customizer);
}
export class UnMergeable {
cloneable = false;
setCloneable(cloneable: any) {
this.cloneable = cloneable;
}
}
export const mergeUtils = {
merge,
cloneDeep,
};

View File

@ -36,7 +36,7 @@
"rollup-plugin-visualizer": "^5.8.2",
"ts-node": "^10.9.1",
"tslib": "^2.5.2",
"typescript": "^4.8.4"
"typescript": "^5.4.2"
},
"gitHead": "be13390b3a9177c9d99f1efabfc285d0c377b013"
}

View File

@ -37,7 +37,7 @@
"rollup-plugin-visualizer": "^5.8.2",
"ts-node": "^10.9.1",
"tslib": "^2.5.2",
"typescript": "^4.8.4"
"typescript": "^5.4.2"
},
"gitHead": "be13390b3a9177c9d99f1efabfc285d0c377b013"
}

View File

@ -53,7 +53,7 @@
"ts-node": "^10.9.1",
"tslib": "^2.5.2",
"typeorm": "^0.3.11",
"typescript": "~5.1.0"
"typescript": "^5.4.2"
},
"gitHead": "be13390b3a9177c9d99f1efabfc285d0c377b013"
}

View File

@ -51,7 +51,7 @@
"rollup-plugin-visualizer": "^5.8.2",
"ts-node": "^10.9.1",
"tslib": "^2.5.2",
"typescript": "^4.8.4",
"typescript": "^5.4.2",
"vite": "^3.1.0",
"vue-tsc": "^0.38.9"
},

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { ref, watch } from "vue";
const props = defineProps<
{
watches: string[];
} & ComponentPropsType
>();
const emit = defineEmits<{
"update:value": any;
}>();
const optionsRef = ref([]);
const getOptions = async () => {
return await doRequest({
type: props.type,
typeName: props.typeName,
action: props.action,
input: props.form
});
};
watch(
() => {
const values = [];
for (const item of props.watches) {
values.push(props.form[item]);
}
return {
form: props.form,
watched: values
};
},
async () => {
optionsRef.value = await getOptions();
},
{ immediate: true }
);
</script>
<template>
<a-select class="remote-select" :options="optionsRef" :value="value" @update:value="emit('update:value', $event)" />
</template>
<style lang="less"></style>

View File

@ -1,6 +1,8 @@
import PiSynologyIdDeviceGetter from "./synology/device-id-getter.vue";
import SynologyIdDeviceGetter from "./synology/device-id-getter.vue";
import RemoteSelect from "./common/remote-select.vue";
export default {
install(app: any) {
app.component("PiSynologyDeviceIdGetter", PiSynologyIdDeviceGetter);
app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter);
app.component("RemoteSelect", RemoteSelect);
}
};

View File

@ -0,0 +1,31 @@
import { request } from "/@/api/service";
export type ComponentPropsType = {
type: string;
typeName: string;
action: string;
form: any;
value?: any;
};
export type RequestHandleReq<T = any> = {
type: strin;
typeName: string;
action: string;
data: any;
input: T;
};
export async function doRequest(req: RequestHandleReq) {
const url = req.type === "access" ? "/pi/handle/access" : "/pi/handle/plugin";
const { typeName, action, data, input } = req;
const res = await request({
url,
method: "post",
data: {
typeName,
action,
data,
input
}
});
return res;
}

View File

@ -11,15 +11,10 @@
<script lang="tsx" setup>
import { defineProps, ref, useAttrs } from "vue";
import { request } from "/@/api/service";
import { Modal } from "ant-design-vue";
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
const props = defineProps<{
type: string;
typeName: string;
form: any;
value?: any;
}>();
const props = defineProps<ComponentPropsType>();
const emit = defineEmits<{
"update:value": any;
@ -29,24 +24,15 @@ const attrs = useAttrs();
const otpCodeRef = ref("");
async function doRequest(action: string, data: any) {
const res = await request({
url: "/pi/handle",
method: "post",
data: {
async function loginWithOTPCode(otpCode: string) {
return await doRequest({
type: props.type,
typeName: props.typeName,
action,
data: data,
input: props.form.access
}
});
return res;
}
async function loginWithOTPCode(otpCode: string) {
return await doRequest("LoginWithOPTCode", {
action: "LoginWithOPTCode",
data: {
otpCode
},
form: props.form
});
}

View File

@ -1,23 +1,19 @@
import _ from "lodash-es";
import { compute } from "@fast-crud/fast-crud";
import { asyncCompute, compute } from "@fast-crud/fast-crud";
import { computed } from "vue";
export type MergeScriptContext = {
compute: typeof compute;
asyncCompute: typeof asyncCompute;
computed: typeof computed;
};
export function useReference(formItem: any) {
if (formItem.reference) {
for (const reference of formItem.reference) {
_.set(
formItem,
reference.dest,
compute<any>((scope) => {
return _.get(scope, reference.src);
})
);
}
delete formItem.reference;
}
if (formItem.mergeScript) {
const ctx = {
compute
compute,
asyncCompute,
computed
};
const script = formItem.mergeScript;
const func = new Function("ctx", script);

View File

@ -60,7 +60,7 @@
"kubernetes-client": "^9.0.0",
"lodash-es": "^4.17.21",
"log4js": "^6.7.1",
"lru-cache": "^10.0.0",
"lru-cache": "^10.2.0",
"md5": "^2.3.0",
"mwtsc": "^1.4.0",
"nanoid": "^4.0.0",
@ -95,7 +95,7 @@
"mwts": "^1.3.0",
"prettier": "^2.8.8",
"ts-node": "^10.9.2",
"typescript": "~5.1.0"
"typescript": "^5.4.2"
},
"engines": {
"node": ">=18.0.0"

View File

@ -1,15 +1,33 @@
import { ALL, Body, Controller, Post, Provide } from '@midwayjs/core';
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { Constants } from '../../../basic/constants.js';
import { accessRegistry, http, logger, PluginRequest, RequestHandleContext } from '@certd/pipeline';
import { merge } from 'lodash-es';
import {
accessRegistry,
AccessRequestHandleContext,
AccessRequestHandleReq,
http,
ITaskPlugin,
logger,
mergeUtils,
pluginRegistry,
PluginRequestHandleReq,
TaskInstanceContext,
utils,
} from '@certd/pipeline';
import { BaseController } from '../../../basic/base-controller.js';
import { AccessService } from '../service/access-service.js';
import { EmailService } from '../../basic/service/email-service.js';
@Provide()
@Controller('/api/pi/handle')
export class HandleController extends BaseController {
@Post('/', { summary: Constants.per.authOnly })
async request(@Body(ALL) body: PluginRequest) {
const type = body.type;
if (type === 'access') {
@Inject()
accessService: AccessService;
@Inject()
emailService: EmailService;
@Post('/access', { summary: Constants.per.authOnly })
async accessRequest(@Body(ALL) body: AccessRequestHandleReq) {
const accessItem = accessRegistry.get(body.typeName);
const accessCls = accessItem.target;
if (accessCls == null) {
@ -18,19 +36,75 @@ export class HandleController extends BaseController {
//实例化access
//@ts-ignore
const access = new accessCls();
//注入input
merge(access, body.input);
const ctx: RequestHandleContext = {
let isNew = true;
if (body.input.id > 0) {
const oldEntity = await this.accessService.info(body.input.id);
if (!oldEntity) {
isNew = false;
const param = {
type: body.typeName,
setting: JSON.stringify(body.input.access),
};
this.accessService.encryptSetting(param, oldEntity);
body.input.access = JSON.parse(param.setting);
}
}
if (isNew) {
mergeUtils.merge(access, body.input.access);
}
const ctx: AccessRequestHandleContext = {
http: http,
logger: logger,
utils,
};
const res = await access.onRequest(body, ctx);
return this.ok(res);
} else if (type === 'plugin') {
throw new Error(`plugin:${body.typeName} not support`);
} else {
throw new Error(`type:${type} not support`);
}
@Post('/plugin', { summary: Constants.per.authOnly })
async pluginRequest(@Body(ALL) body: PluginRequestHandleReq) {
const pluginDefine = pluginRegistry.get(body.typeName);
const pluginCls = pluginDefine.target;
if (pluginCls == null) {
throw new Error(`plugin ${body.typeName} not found`);
}
//实例化access
//@ts-ignore
const plugin: PluginRequestHandler = new pluginCls();
//@ts-ignore
const instance = plugin as ITaskPlugin;
//@ts-ignore
const taskCtx: TaskInstanceContext = {
pipeline: undefined,
step: undefined,
lastStatus: undefined,
http,
logger: logger,
inputChanged: true,
accessService: this.accessService,
emailService: this.emailService,
pipelineContext: undefined,
userContext: undefined,
fileStore: undefined,
signal: undefined,
// pipelineContext: this.pipelineContext,
// userContext: this.contextFactory.getContext('user', this.options.userId),
// fileStore: new FileStore({
// scope: this.pipeline.id,
// parent: this.runtime.id,
// rootDir: this.options.fileRootDir,
// }),
// signal: this.abort.signal,
utils,
};
instance.setCtx(taskCtx);
mergeUtils.merge(plugin, body.input);
await instance.onInstance();
const res = await plugin.onRequest(body);
return this.ok(res);
}
}

View File

@ -2,8 +2,8 @@
"compileOnSave": true,
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node16",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,