From 608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sun, 5 Jul 2026 01:14:48 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E9=AA=8C=EF=BC=8C=E9=A6=96=E6=AC=A1=E8=AE=BF=E9=97=AE?= =?UTF-8?q?=E6=97=B6=E5=BC=B9=E5=87=BA=E9=82=AE=E7=AE=B1=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E7=BB=91=E5=AE=9A=E7=94=A8=E4=BB=A5=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- .../lib-server/src/basic/base-controller.ts | 89 ++++++----- .../libs/lib-server/src/basic/base-service.ts | 2 +- .../system/settings/entity/sys-settings.ts | 24 +-- .../src/system/settings/service/models.ts | 142 +++++++++--------- .../ui/certd-client/src/use/use-dialog.ts | 9 +- .../certd/mine/change-password-button.vue | 15 +- .../src/views/framework/home/index.vue | 97 +++++++++++- .../controller/user/mine/mine-controller.ts | 80 +++++++++- .../runtime-deps/runtime-deps-service.ts | 3 +- 10 files changed, 319 insertions(+), 144 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index df2c07d4b..ce2279a72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,7 +102,7 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web - 只有需要事务传播时才定义 `ctx`;普通查询、纯函数和简单私有方法继续使用明确参数。 - 需要按事务上下文取 Repository 时,用 `BaseService.getRepo(ctx, EntityClass)`。 - 需要“有事务则复用、无事务则开启”时,用 `BaseService.transactionWithCtx(ctx, callback)`。 -- 拼接可选 `projectId` 查询条件时,用 `BaseService.buildUserProjectQuery(userId, projectId)`;不要直接写 `{ userId, projectId }`。 +- 拼接可选 `projectId` 查询条件时,**必须**使用 `BaseService.buildUserProjectQuery(userId, projectId)`,禁止直接写 `{ userId, projectId }`。因为 `projectId` 可能为 `null`/`undefined`,直接放入查询会生成错误的 `WHERE projectId = NULL` 条件。 - `ctx` 类型复用 `BaseService` 导出的 `ServiceContext`。 - 新增 service 方法避免与 `BaseService` 方法签名冲突,例如不要用 `delete(id)` 覆盖 `delete(ids, where?)`;改用 `deleteById` 等具体名称。 diff --git a/packages/libs/lib-server/src/basic/base-controller.ts b/packages/libs/lib-server/src/basic/base-controller.ts index 0de3dff1f..5149721ef 100644 --- a/packages/libs/lib-server/src/basic/base-controller.ts +++ b/packages/libs/lib-server/src/basic/base-controller.ts @@ -1,9 +1,8 @@ -import { ApplicationContext, Inject } from '@midwayjs/core'; -import type {IMidwayContainer} from '@midwayjs/core'; -import * as koa from '@midwayjs/koa'; -import { Constants } from './constants.js'; -import { isEnterprise } from './mode.js'; - +import { ApplicationContext, Inject } from "@midwayjs/core"; +import type { IMidwayContainer } from "@midwayjs/core"; +import * as koa from "@midwayjs/koa"; +import { Constants } from "./constants.js"; +import { isEnterprise } from "./mode.js"; export abstract class BaseController { @Inject() @@ -41,7 +40,7 @@ export abstract class BaseController { getUserId() { const userId = this.ctx.user?.id; if (userId == null) { - throw new Error('Token已过期'); + throw new Error("Token已过期"); } return userId; } @@ -49,7 +48,7 @@ export abstract class BaseController { getLoginUser() { const user = this.ctx.user; if (user == null) { - throw new Error('Token已过期'); + throw new Error("Token已过期"); } return user; } @@ -61,73 +60,71 @@ export abstract class BaseController { } } - async getProjectId(permission:string) { + async getProjectId(permission: string) { if (!isEnterprise()) { - return undefined + return undefined; } let projectIdStr = this.ctx.headers["project-id"] as string; - if (!projectIdStr){ + if (!projectIdStr) { projectIdStr = this.ctx.request.query["projectId"] as string; } if (!projectIdStr) { //这里必须抛异常,否则可能会有权限问题 - throw new Error("projectId 不能为空") + throw new Error("projectId 不能为空"); } - const userId = this.getUserId() - const projectId = parseInt(projectIdStr) - await this.checkProjectPermission(userId, projectId,permission) + const userId = this.getUserId(); + const projectId = parseInt(projectIdStr); + await this.checkProjectPermission(userId, projectId, permission); return projectId; } - async getProjectUserId(permission:string){ - let userId = this.getUserId() - const projectId = await this.getProjectId(permission) - if(projectId){ - userId = -1 // 企业管理模式下,用户id固定-1 + async getProjectUserId(permission: string) { + let userId = this.getUserId(); + const projectId = await this.getProjectId(permission); + if (projectId) { + userId = -1; // 企业管理模式下,用户id固定-1 } return { - projectId,userId - } + projectId, + userId, + }; } - async getProjectUserIdRead(){ - return await this.getProjectUserId("read") + async getProjectUserIdRead() { + return await this.getProjectUserId("read"); } - async getProjectUserIdWrite(){ - return await this.getProjectUserId("write") + async getProjectUserIdWrite() { + return await this.getProjectUserId("write"); } - async getProjectUserIdAdmin(){ - return await this.getProjectUserId("admin") + async getProjectUserIdAdmin() { + return await this.getProjectUserId("admin"); } - async checkProjectPermission(userId: number, projectId: number,permission:string) { - const projectService:any = await this.applicationContext.getAsync("projectService"); - await projectService.checkPermission({userId,projectId,permission}) + async checkProjectPermission(userId: number, projectId: number, permission: string) { + const projectService: any = await this.applicationContext.getAsync("projectService"); + await projectService.checkPermission({ userId, projectId, permission }); } /** - * + * * @param service 检查记录是否属于某用户或某项目 - * @param id + * @param id */ - async checkOwner(service:any,id:number,permission:string,allowAdmin:boolean = false){ - let { projectId,userId } = await this.getProjectUserId(permission) - const authService:any = await this.applicationContext.getAsync("authService"); + async checkOwner(service: any, id: number, permission: string, allowAdmin: boolean = false) { + const { projectId, userId } = await this.getProjectUserId(permission); + const authService: any = await this.applicationContext.getAsync("authService"); if (projectId) { await authService.checkProjectId(service, id, projectId); - }else{ - - if(userId === Constants.systemUserId){ + } else { + if (userId === Constants.systemUserId) { //系统级别,不检查权限 - }else{ - if(allowAdmin){ + } else { + if (allowAdmin) { await authService.checkUserIdButAllowAdmin(this.ctx, service, id); - }else{ - await authService.checkUserId( service, id, userId); + } else { + await authService.checkUserId(service, id, userId); } } - } - return {projectId,userId} + return { projectId, userId }; } - } diff --git a/packages/libs/lib-server/src/basic/base-service.ts b/packages/libs/lib-server/src/basic/base-service.ts index 8c21b7ae5..ca83591df 100644 --- a/packages/libs/lib-server/src/basic/base-service.ts +++ b/packages/libs/lib-server/src/basic/base-service.ts @@ -56,7 +56,7 @@ export abstract class BaseService { return dataSource.getRepository(entity); } - protected buildUserProjectQuery(userId: number, projectId?: number) { + public buildUserProjectQuery(userId: number, projectId?: number) { const query: { userId: number; projectId?: number; [key: string]: any } = { userId, }; diff --git a/packages/libs/lib-server/src/system/settings/entity/sys-settings.ts b/packages/libs/lib-server/src/system/settings/entity/sys-settings.ts index 9c7139815..ce38f29cd 100644 --- a/packages/libs/lib-server/src/system/settings/entity/sys-settings.ts +++ b/packages/libs/lib-server/src/system/settings/entity/sys-settings.ts @@ -1,33 +1,33 @@ -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; /** */ -@Entity('sys_settings') +@Entity("sys_settings") export class SysSettingsEntity { @PrimaryGeneratedColumn() id: number; - @Column({ comment: 'key', length: 100 }) + @Column({ comment: "key", length: 100 }) key: string; - @Column({ comment: '名称', length: 100 }) + @Column({ comment: "名称", length: 100 }) title: string; - @Column({ name: 'setting', comment: '设置', length: 1024, nullable: true }) + @Column({ name: "setting", comment: "设置", length: 1024, nullable: true }) setting: string; // public 公开读,私有写, private 私有读,私有写 - @Column({ name: 'access', comment: '访问权限' }) + @Column({ name: "access", comment: "访问权限" }) access: string; @Column({ - name: 'create_time', - comment: '创建时间', - default: () => 'CURRENT_TIMESTAMP', + name: "create_time", + comment: "创建时间", + default: () => "CURRENT_TIMESTAMP", }) createTime: Date; @Column({ - name: 'update_time', - comment: '修改时间', - default: () => 'CURRENT_TIMESTAMP', + name: "update_time", + comment: "修改时间", + default: () => "CURRENT_TIMESTAMP", }) updateTime: Date; } diff --git a/packages/libs/lib-server/src/system/settings/service/models.ts b/packages/libs/lib-server/src/system/settings/service/models.ts index 13f701ac4..f14e29a87 100644 --- a/packages/libs/lib-server/src/system/settings/service/models.ts +++ b/packages/libs/lib-server/src/system/settings/service/models.ts @@ -1,19 +1,19 @@ -import { cloneDeep } from 'lodash-es'; +import { cloneDeep } from "lodash-es"; export class BaseSettings { static __key__: string; static __title__: string; - static __access__ = 'private'; + static __access__ = "private"; static getCacheKey() { - return 'settings.' + this.__key__; + return "settings." + this.__key__; } } export class SysPublicSettings extends BaseSettings { - static __key__ = 'sys.public'; - static __title__ = '系统公共设置'; - static __access__ = 'public'; + static __key__ = "sys.public"; + static __title__ = "系统公共设置"; + static __access__ = "public"; registerEnabled = false; userValidTimeEnabled?: boolean = false; @@ -34,19 +34,15 @@ export class SysPublicSettings extends BaseSettings { aiChatEnabled = true; homePageEnabled = true; - //验证码是否开启 captchaEnabled = false; //验证码类型 captchaType?: string; captchaAddonId?: number; - - //流水线是否启用有效期 pipelineValidTimeEnabled?: boolean = false; - //证书域名添加到监控 certDomainAddToMonitorEnabled?: boolean = false; @@ -60,12 +56,15 @@ export class SysPublicSettings extends BaseSettings { // 第三方OAuth配置 oauthEnabled?: boolean = false; - oauthProviders: Record = {}; + oauthProviders: Record< + string, + { + type: string; + title: string; + addonId: number; + icon?: string; + } + > = {}; notice?: string; @@ -73,40 +72,37 @@ export class SysPublicSettings extends BaseSettings { } export class SysPrivateSettings extends BaseSettings { - static __title__ = '系统私有设置'; - static __access__ = 'private'; - static __key__ = 'sys.private'; + static __title__ = "系统私有设置"; + static __access__ = "private"; + static __key__ = "sys.private"; jwtKey?: string; encryptSecret?: string; - httpsProxy? = ''; - httpProxy? = ''; - noProxy? = ''; - commonHeaders?: string = ''; + httpsProxy? = ""; + httpProxy? = ""; + noProxy? = ""; + commonHeaders?: string = ""; reverseProxies?: Record = {}; - dnsResultOrder? = ''; + dnsResultOrder? = ""; commonCnameEnabled?: boolean = true; httpRequestTimeout?: number = 30; pipelineMaxRunningCount?: number; - - environmentVars?: string = ''; - + environmentVars?: string = ""; acmeWalkFromAuthoritative?: boolean = true; - sms?: { type?: string; config?: any; } = { - type: 'aliyun', - config: {}, - }; + type: "aliyun", + config: {}, + }; removeSecret() { const clone = cloneDeep(this); @@ -117,9 +113,9 @@ export class SysPrivateSettings extends BaseSettings { } export class SysInstallInfo extends BaseSettings { - static __title__ = '系统安装信息'; - static __key__ = 'sys.install'; - static __access__ = 'private'; + static __title__ = "系统安装信息"; + static __key__ = "sys.install"; + static __access__ = "private"; installTime?: number; siteId?: string; bindUserId?: number; @@ -130,21 +126,20 @@ export class SysInstallInfo extends BaseSettings { } export class SysLicenseInfo extends BaseSettings { - static __title__ = '授权许可信息'; - static __key__ = 'sys.license'; - static __access__ = 'private'; + static __title__ = "授权许可信息"; + static __key__ = "sys.license"; + static __access__ = "private"; license?: string; } - export type EmailTemplate = { addonId?: number; -} +}; export class SysEmailConf extends BaseSettings { - static __title__ = '邮箱配置'; - static __key__ = 'sys.email'; - static __access__ = 'private'; + static __title__ = "邮箱配置"; + static __key__ = "sys.email"; + static __access__ = "private"; host: string; port: number; @@ -160,18 +155,18 @@ export class SysEmailConf extends BaseSettings { sender: string; usePlus?: boolean; - templates:{ - registerCode?: EmailTemplate, - forgotPassword?: EmailTemplate, - pipelineResult?: EmailTemplate, - common?: EmailTemplate, - } + templates: { + registerCode?: EmailTemplate; + forgotPassword?: EmailTemplate; + pipelineResult?: EmailTemplate; + common?: EmailTemplate; + }; } export class SysSiteInfo extends BaseSettings { - static __title__ = '站点信息'; - static __key__ = 'sys.site'; - static __access__ = 'public'; + static __title__ = "站点信息"; + static __key__ = "sys.site"; + static __access__ = "public"; title?: string; slogan?: string; logo?: string; @@ -179,9 +174,9 @@ export class SysSiteInfo extends BaseSettings { } export class SysSecretBackup extends BaseSettings { - static __title__ = '密钥信息备份'; - static __key__ = 'sys.secret.backup'; - static __access__ = 'private'; + static __title__ = "密钥信息备份"; + static __key__ = "sys.secret.backup"; + static __access__ = "private"; siteId?: string; encryptSecret?: string; } @@ -190,9 +185,9 @@ export class SysSecretBackup extends BaseSettings { * 不要修改 */ export class SysSecret extends BaseSettings { - static __title__ = '密钥信息'; - static __key__ = 'sys.secret'; - static __access__ = 'private'; + static __title__ = "密钥信息"; + static __key__ = "sys.secret"; + static __access__ = "private"; siteId?: string; encryptSecret?: string; } @@ -215,9 +210,9 @@ export type MenuItem = { children?: MenuItem[]; }; export class SysHeaderMenus extends BaseSettings { - static __title__ = '顶部菜单'; - static __key__ = 'sys.header.menus'; - static __access__ = 'public'; + static __title__ = "顶部菜单"; + static __key__ = "sys.header.menus"; + static __access__ = "public"; menus: MenuItem[]; } @@ -228,9 +223,9 @@ export type PaymentItem = { }; export class SysPaymentSetting extends BaseSettings { - static __title__ = '支付设置'; - static __key__ = 'sys.payment'; - static __access__ = 'private'; + static __title__ = "支付设置"; + static __key__ = "sys.payment"; + static __access__ = "private"; yizhifu?: PaymentItem = { enabled: false }; @@ -240,9 +235,9 @@ export class SysPaymentSetting extends BaseSettings { } export class SysSuiteSetting extends BaseSettings { - static __title__ = '套餐设置'; - static __key__ = 'sys.suite'; - static __access__ = 'private'; + static __title__ = "套餐设置"; + static __key__ = "sys.suite"; + static __access__ = "private"; enabled: boolean = false; @@ -257,26 +252,25 @@ export class SysSuiteSetting extends BaseSettings { } export class SysAutoFixSetting extends BaseSettings { - static __title__ = '自动修复记录'; - static __key__ = 'sys.auto.fix'; - static __access__ = 'private'; + static __title__ = "自动修复记录"; + static __key__ = "sys.auto.fix"; + static __access__ = "private"; fixed: Record = {}; } - export type SiteHidden = { enabled: boolean; openPath?: string; //md5 hash 两次后保存 openPassword?: string; autoHiddenTimes?: number; - hiddenOpenApi?: boolean + hiddenOpenApi?: boolean; }; export class SysSafeSetting extends BaseSettings { - static __title__ = '站点安全设置'; - static __key__ = 'sys.safe'; - static __access__ = 'private'; + static __title__ = "站点安全设置"; + static __key__ = "sys.safe"; + static __access__ = "private"; // 站点隐藏 hidden: SiteHidden = { diff --git a/packages/ui/certd-client/src/use/use-dialog.ts b/packages/ui/certd-client/src/use/use-dialog.ts index 71421b0e5..3dc2a8d45 100644 --- a/packages/ui/certd-client/src/use/use-dialog.ts +++ b/packages/ui/certd-client/src/use/use-dialog.ts @@ -31,6 +31,13 @@ export function useFormDialog() { crudOptions: { columns: req.columns, form: { + labelCol: { + // @ts-ignore + span: null, + style: { + width: "100px", + }, + }, initialForm: req.initialForm, wrapper: warpper, async afterSubmit() {}, @@ -44,7 +51,7 @@ export function useFormDialog() { }; } const { crudOptions } = createCrudOptions(); - await openCrudFormDialog({ crudOptions }); + return await openCrudFormDialog({ crudOptions }); } return { openFormDialog, diff --git a/packages/ui/certd-client/src/views/certd/mine/change-password-button.vue b/packages/ui/certd-client/src/views/certd/mine/change-password-button.vue index ed2f2ec3c..067a4450f 100644 --- a/packages/ui/certd-client/src/views/certd/mine/change-password-button.vue +++ b/packages/ui/certd-client/src/views/certd/mine/change-password-button.vue @@ -18,6 +18,10 @@ defineProps<{ showButton: boolean; }>(); +const emit = defineEmits<{ + (e: "close"): void; +}>(); + let passwordFormRef = ref(); type OpenOptions = { @@ -68,8 +72,8 @@ const passwordFormOptions: CrudOptions = { }, async afterSubmit() { const formData = passwordFormRef.value?.getFormData?.(); - const message = formData?.init ? t("authentication.initPasswordSuccessMessage") : t("authentication.successMessage"); - notification.success({ message }); + const msg = formData?.init ? t("authentication.initPasswordSuccessMessage") : t("authentication.successMessage"); + notification.success({ message: msg }); }, }, columns: { @@ -84,6 +88,7 @@ const passwordFormOptions: CrudOptions = { title: t("authentication.oldPassword"), type: "password", form: { + //@ts-ignore show: compute(({ form }) => form.init !== true), rules: [{ required: true, message: t("authentication.oldPasswordRequired") }], }, @@ -118,16 +123,18 @@ const passwordFormOptions: CrudOptions = { async function open(opts: OpenOptions = {}) { const formOptions = buildFormOptions(passwordFormOptions); - formOptions.newInstance = true; //新实例打开 + formOptions.newInstance = true; if (opts.init) { formOptions.wrapper.title = t("authentication.initPasswordTitle"); } + formOptions.wrapper.onClosed = () => { + emit("close"); + }; passwordFormRef.value = await openDialog(formOptions); passwordFormRef.value.setFormData({ init: opts.init === true, password: opts.password || "", }); - console.log(passwordFormRef.value); } const scope = ref({ diff --git a/packages/ui/certd-client/src/views/framework/home/index.vue b/packages/ui/certd-client/src/views/framework/home/index.vue index cde8abff1..78c096e93 100644 --- a/packages/ui/certd-client/src/views/framework/home/index.vue +++ b/packages/ui/certd-client/src/views/framework/home/index.vue @@ -2,22 +2,110 @@ - + - diff --git a/packages/ui/certd-server/src/controller/user/mine/mine-controller.ts b/packages/ui/certd-server/src/controller/user/mine/mine-controller.ts index fde30a218..25addd26e 100644 --- a/packages/ui/certd-server/src/controller/user/mine/mine-controller.ts +++ b/packages/ui/certd-server/src/controller/user/mine/mine-controller.ts @@ -1,10 +1,14 @@ -import { BaseController, Constants, SysSettingsService } from "@certd/lib-server"; +import { AccessGetter, AccessService, BaseController, Constants, SysSettingsService } from "@certd/lib-server"; import { ALL, Body, Controller, Inject, Post, Provide } from "@midwayjs/core"; import { PasskeyService } from "../../../modules/login/service/passkey-service.js"; import { RoleService } from "../../../modules/sys/authority/service/role-service.js"; import { UserService } from "../../../modules/sys/authority/service/user-service.js"; +import { NotificationService } from "../../../modules/pipeline/service/notification-service.js"; +import { newAccess } from "@certd/pipeline"; +import { http, logger, utils } from "@certd/basic"; import { ApiTags } from "@midwayjs/swagger"; import { CodeService } from "../../../modules/basic/service/code-service.js"; +import { EmailService } from "../../../modules/basic/service/email-service.js"; /** */ @@ -27,6 +31,15 @@ export class MineController extends BaseController { @Inject() sysSettingsService: SysSettingsService; + @Inject() + accessService: AccessService; + + @Inject() + notificationService: NotificationService; + + @Inject() + emailService: EmailService; + @Post("/info", { description: Constants.per.authOnly, summary: "查询用户信息" }) public async info() { const userId = this.getUserId(); @@ -41,6 +54,17 @@ export class MineController extends BaseController { delete user.password; //@ts-ignore user.needInitPassword = needInitPassword; + + const { projectId } = await this.getProjectUserIdRead(); + const userProjectQuery = this.accessService.buildUserProjectQuery(userId, projectId); + const existingAccess = await this.accessService.findOne({ + where: { type: "acmeAccount", subtype: "letsencrypt", ...userProjectQuery }, + }); + if (!existingAccess) { + //@ts-ignore + user.needInitAccount = true; + } + return this.ok(user); } @@ -122,4 +146,58 @@ export class MineController extends BaseController { }); return this.ok({}); } + + @Post("/accountInit", { description: Constants.per.authOnly, summary: "初始化Let's Encrypt ACME账号和邮件通知" }) + public async accountInit(@Body("email") email?: string) { + const { projectId, userId } = await this.getProjectUserIdWrite(); + + let userEmail = email; + let user: any = null; + if (!userEmail) { + user = await this.userService.info(userId); + userEmail = user.email; + } + if (!userEmail) { + return this.ok({ needEmail: true }); + } + + if (email) { + if (!user) { + user = await this.userService.info(userId); + } + if (!user.email) { + await this.userService.updateEmail(userId, { email: userEmail }); + } + } + + await this.emailService.add(userId, userEmail); + + await this.notificationService.getOrCreateDefault(userEmail, userId, projectId); + + const getAccessById = this.accessService.getById.bind(this.accessService); + const accessGetter = new AccessGetter(userId, projectId, getAccessById); + const accessContext = { + http, + logger, + utils, + accessService: accessGetter, + define: undefined, + } as any; + const access = await newAccess("acmeAccount", { caType: "letsencrypt", email: userEmail }, accessGetter, accessContext); + const accountJson = await access.onGenerateAccount(); + + await this.accessService.add({ + type: "acmeAccount", + name: "Let's Encrypt", + userId, + projectId, + setting: JSON.stringify({ + caType: "letsencrypt", + email: userEmail, + account: accountJson, + }), + }); + + return this.ok({ success: true }); + } } diff --git a/packages/ui/certd-server/src/modules/runtime-deps/runtime-deps-service.ts b/packages/ui/certd-server/src/modules/runtime-deps/runtime-deps-service.ts index f131f1dae..1e76d6875 100644 --- a/packages/ui/certd-server/src/modules/runtime-deps/runtime-deps-service.ts +++ b/packages/ui/certd-server/src/modules/runtime-deps/runtime-deps-service.ts @@ -441,7 +441,8 @@ export class RuntimeDepsService { private getDefineByPluginKey(pluginKey: string, owner?: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine { const parts = pluginKey.split(":"); - let [pluginType, subtype, name] = parts; + const [pluginType, subtype, rawName] = parts; + let name = rawName; if (parts.length === 2) { name = subtype; } else if (parts.length === 3) {