From f9541fab701e01ba57af061da322204c894adfb8 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Wed, 10 Jun 2026 23:32:39 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=96=B0=E5=A2=9E=E7=AB=99=E7=82=B9?= =?UTF-8?q?=E8=AF=81=E4=B9=A6=E7=9B=91=E6=8E=A7=E4=BB=8EDNS=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E8=AE=B0=E5=BD=95=E6=89=B9=E9=87=8F=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交新增了从DNS解析记录批量导入站点监控的完整功能: 1. 扩展Registrable类型新增icon字段支持 2. 新增DNS解析记录获取接口和基础实现 3. 为阿里云、腾讯云、Cloudflare等DNS提供商添加解析记录分页获取支持 4. 新增站点监控导入任务管理功能,支持保存、启动、删除导入任务 5. 新增中文/英文多语言支持 6. 优化暗黑模式表格样式 7. 修复ACME账户访问修复逻辑中项目ID可选的问题 8. 优化HiPM DNS提供商的域名获取逻辑 --- packages/core/basic/build.md | 2 +- .../core/pipeline/src/registry/registry.ts | 1 + .../plugin-lib/src/cert/dns-provider/api.ts | 10 + .../plugin-lib/src/cert/dns-provider/base.ts | 6 +- .../locales/langs/en-US/certd/cert-domain.ts | 1 + .../locales/langs/zh-CN/certd/cert-domain.ts | 1 + packages/ui/certd-client/src/style/dark.less | 41 +++ .../src/views/certd/monitor/site/api.ts | 30 ++ .../src/views/certd/monitor/site/crud.tsx | 22 +- .../src/views/certd/monitor/site/import.vue | 139 ++++++++++ .../src/views/certd/monitor/site/use.tsx | 115 +++++++- .../metadata/deploy_CertApply.yaml | 8 +- .../user/monitor/site-info-controller.ts | 49 ++++ .../fix/legacy-acme-account-access-fix.ts | 11 +- .../src/modules/mine/service/models.ts | 7 + .../monitor/service/site-info-service.ts | 262 ++++++++++++++++-- .../dns-provider/aliyun-dns-provider.ts | 31 ++- .../plugins/plugin-cloudflare/dns-provider.ts | 30 +- .../dns-provider/hipmdnsmgr-dns-provider.ts | 10 - .../dns-provider/tencent-dns-provider.ts | 26 +- trigger/build.trigger | 2 +- 21 files changed, 749 insertions(+), 55 deletions(-) create mode 100644 packages/ui/certd-client/src/views/certd/monitor/site/import.vue diff --git a/packages/core/basic/build.md b/packages/core/basic/build.md index bb6eb5c27..433dc5317 100644 --- a/packages/core/basic/build.md +++ b/packages/core/basic/build.md @@ -1 +1 @@ -02:33 +23:28 diff --git a/packages/core/pipeline/src/registry/registry.ts b/packages/core/pipeline/src/registry/registry.ts index cdd1ff92c..8dd8f311e 100644 --- a/packages/core/pipeline/src/registry/registry.ts +++ b/packages/core/pipeline/src/registry/registry.ts @@ -7,6 +7,7 @@ export type Registrable = { group?: string; deprecated?: string; order?: number; + icon?: string; }; export type TargetGetter = () => Promise; export type RegistryItem = { diff --git a/packages/plugins/plugin-lib/src/cert/dns-provider/api.ts b/packages/plugins/plugin-lib/src/cert/dns-provider/api.ts index 11bcb8579..70ea506c7 100644 --- a/packages/plugins/plugin-lib/src/cert/dns-provider/api.ts +++ b/packages/plugins/plugin-lib/src/cert/dns-provider/api.ts @@ -34,6 +34,14 @@ export type DomainRecord = { domain: string; }; +export type DnsResolveRecord = { + id: string; + hostRecord: string; + fullRecord: string; + type: string; + value: string; +}; + export interface IDnsProvider { onInstance(): Promise; @@ -59,6 +67,8 @@ export interface IDnsProvider { usePunyCode(): boolean; getDomainListPage(pager: PageSearch): Promise>; + + getRecordListPage?(domain: string, pager: PageSearch): Promise>; } export interface ISubDomainsGetter { diff --git a/packages/plugins/plugin-lib/src/cert/dns-provider/base.ts b/packages/plugins/plugin-lib/src/cert/dns-provider/base.ts index 158b51e57..cd153ce2f 100644 --- a/packages/plugins/plugin-lib/src/cert/dns-provider/base.ts +++ b/packages/plugins/plugin-lib/src/cert/dns-provider/base.ts @@ -1,7 +1,7 @@ import { HttpClient, ILogger } from "@certd/basic"; import { IAccessService, PageRes, PageSearch } from "@certd/pipeline"; import punycode from "punycode.js"; -import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js"; +import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DnsResolveRecord, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js"; import { dnsProviderRegistry } from "./registry.js"; export abstract class AbstractDnsProvider implements IDnsProvider { ctx!: DnsProviderContext; @@ -49,6 +49,10 @@ export abstract class AbstractDnsProvider implements IDnsProvider { async getDomainListPage(req: PageSearch): Promise> { throw new Error("Method not implemented."); } + + async getRecordListPage(domain: string, req: PageSearch): Promise> { + throw new Error("Method not implemented."); + } } export async function createDnsProvider(opts: { dnsProviderType: string; context: DnsProviderContext }): Promise { diff --git a/packages/ui/certd-client/src/locales/langs/en-US/certd/cert-domain.ts b/packages/ui/certd-client/src/locales/langs/en-US/certd/cert-domain.ts index 01387a475..53cdd1b66 100644 --- a/packages/ui/certd-client/src/locales/langs/en-US/certd/cert-domain.ts +++ b/packages/ui/certd-client/src/locales/langs/en-US/certd/cert-domain.ts @@ -18,6 +18,7 @@ export default { subdomainConfirmTitle: "Subdomain Confirmation", subdomainConfirmContent: "{domain} appears to be a subdomain. Only delegated subdomains and free second-level subdomains need to be maintained here. Otherwise certificate application may fail. Continue?", importFromProvider: "Import from Domain Provider", + importFromResolveRecords: "Import from DNS Records", syncExpirationDate: "Sync Domain Expiration Time", syncTaskSubmitted: "Sync task submitted", syncExpirationProgress: "Sync Domain Expiration Progress", diff --git a/packages/ui/certd-client/src/locales/langs/zh-CN/certd/cert-domain.ts b/packages/ui/certd-client/src/locales/langs/zh-CN/certd/cert-domain.ts index 9ef745b9a..3be11d9f8 100644 --- a/packages/ui/certd-client/src/locales/langs/zh-CN/certd/cert-domain.ts +++ b/packages/ui/certd-client/src/locales/langs/zh-CN/certd/cert-domain.ts @@ -18,6 +18,7 @@ export default { subdomainConfirmTitle: "子域名确认", subdomainConfirmContent: "检测到{domain}为子域名,只有托管子域名和免费二级子域名才需要在此处维护,否则会导致申请证书失败,请确认是否继续?", importFromProvider: "从域名提供商导入", + importFromResolveRecords: "从解析记录导入", syncExpirationDate: "同步域名过期时间", syncTaskSubmitted: "同步任务已提交", syncExpirationProgress: "同步域名过期时间进度", diff --git a/packages/ui/certd-client/src/style/dark.less b/packages/ui/certd-client/src/style/dark.less index a21438981..4299bccb4 100644 --- a/packages/ui/certd-client/src/style/dark.less +++ b/packages/ui/certd-client/src/style/dark.less @@ -8,4 +8,45 @@ .vben-normal-menu__item.is-active { background-color: #3b3b3b !important; } + + .cd-table { + th, + td { + border-bottom: 1px solid #303030; + border-left: 1px solid #303030; + } + + th { + background: #1f1f1f; + color: rgba(255, 255, 255, 0.85); + border-top: 1px solid #303030; + + &:last-child { + border-right: 1px solid #303030; + } + } + + td { + &:last-child { + border-right: 1px solid #303030; + } + } + + td.position-sticky-right { + background-color: #141414; + } + + .position-sticky-right::before { + background: #303030; + } + + tr.hover-color:hover td { + background: #262626; + } + + .status-active { + background: #1f3a23; + color: #81c784; + } + } } diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/api.ts b/packages/ui/certd-client/src/views/certd/monitor/site/api.ts index c1ebe1bde..e75eb7d3b 100644 --- a/packages/ui/certd-client/src/views/certd/monitor/site/api.ts +++ b/packages/ui/certd-client/src/views/certd/monitor/site/api.ts @@ -72,6 +72,36 @@ export const siteInfoApi = { }); }, + async ImportTaskSave(body: any) { + return await request({ + url: apiPrefix + "/import/save", + method: "post", + data: body, + }); + }, + async ImportTaskStatus() { + return await request({ + url: apiPrefix + "/import/status", + method: "post", + }); + }, + async ImportTaskDelete(key: string) { + return await request({ + url: apiPrefix + "/import/delete", + method: "post", + data: { key }, + }); + }, + async ImportTaskStart(key: string) { + return await request({ + url: apiPrefix + "/import/start", + method: "post", + data: { key }, + }); + }, + + + async DisabledChange(id: number, disabled: boolean) { return await request({ url: apiPrefix + "/disabledChange", diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx b/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx index 2c2433a47..c0239128e 100644 --- a/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx +++ b/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx @@ -9,7 +9,7 @@ import { useSettingStore } from "/@/store/settings"; import { mySuiteApi } from "/@/views/certd/suite/mine/api"; import { mitter } from "/@/utils/util.mitt"; import { useSiteIpMonitor } from "./ip/use"; -import { useSiteImport } from "/@/views/certd/monitor/site/use"; +import { useSiteImport, useSiteImportTaskManage } from "/@/views/certd/monitor/site/use"; import { ref } from "vue"; import GroupSelector from "../../basic/group/group-selector.vue"; import { createGroupDictRef } from "../../basic/group/api"; @@ -53,6 +53,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat const { openSiteIpMonitorDialog } = useSiteIpMonitor(); const { openSiteImportDialog } = useSiteImport(); + const openSiteImportTaskManageDialog = useSiteImportTaskManage(); const certValidDaysRef = ref(10); @@ -200,6 +201,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat actionbar: { buttons: { add: { + icon: "ion:add-circle-outline", async click() { if (!settingsStore.isPlus) { // 非plus @@ -236,6 +238,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat show: hasActionPermission("write"), text: t("monitor.bulkImport"), type: "primary", + icon: "ion:cloud-upload-outline", async click() { const defaultGroupId = getDefaultGroupId(); openSiteImportDialog({ @@ -246,10 +249,27 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat }); }, }, + importFromProvider: { + show: hasActionPermission("write"), + title: t("certd.domain.importFromResolveRecords"), + text: t("certd.domain.importFromResolveRecords"), + type: "primary", + needPlus: true, + color: "gold", + icon: "mingcute:vip-1-line", + click: async () => { + await openSiteImportTaskManageDialog({ + afterSubmit: () => { + crudExpose.doRefresh(); + }, + }); + }, + }, checkAll: { show: true, text: t("monitor.checkAll"), type: "primary", + icon: "ion:play-circle-outline", click() { checkAll(); }, diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/import.vue b/packages/ui/certd-client/src/views/certd/monitor/site/import.vue new file mode 100644 index 000000000..ec920acc1 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/monitor/site/import.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/use.tsx b/packages/ui/certd-client/src/views/certd/monitor/site/use.tsx index 614496d2a..f6b8eb98d 100644 --- a/packages/ui/certd-client/src/views/certd/monitor/site/use.tsx +++ b/packages/ui/certd-client/src/views/certd/monitor/site/use.tsx @@ -1,7 +1,11 @@ -import { useFormWrapper } from "@fast-crud/fast-crud"; +import { useFormWrapper, compute } from "@fast-crud/fast-crud"; import { siteInfoApi } from "./api"; -import { useI18n } from "/src/locales"; +import { useI18n } from "/@/locales"; +import { useSettingStore } from "/@/store/settings"; +import { useFormDialog } from "/@/use/use-dialog"; import GroupSelector from "../../basic/group/group-selector.vue"; +import SiteInfoImportTaskStatus from "./import.vue"; + export function useSiteImport() { const { t } = useI18n(); const { openCrudFormDialog } = useFormWrapper(); @@ -13,7 +17,7 @@ export function useSiteImport() { columns: { text: { type: "textarea", - title: t("certd.domainList.title"), // 域名列表 + title: t("certd.domainList.title"), form: { helper: t("certd.domainList.helper"), rules: [{ required: true, message: t("certd.domainList.required") }], @@ -21,9 +25,7 @@ export function useSiteImport() { placeholder: t("certd.domainList.placeholder"), rows: 8, }, - col: { - span: 24, - }, + col: { span: 24 }, }, }, groupId: { @@ -36,13 +38,10 @@ export function useSiteImport() { vModel: "modelValue", type: "site", }, - col: { - span: 24, - }, + col: { span: 24 }, }, }, }, - form: { async doSubmit({ form }) { return siteInfoApi.Import(form); @@ -53,7 +52,99 @@ export function useSiteImport() { }); } - return { - openSiteImportDialog, + return { openSiteImportDialog }; +} + +export function useSiteImportTask() { + const { openFormDialog } = useFormDialog(); + const { t } = useI18n(); + + const columns = { + dnsProviderType: { + title: t("certd.domain.domainProvider"), + type: "text", + form: { + component: { + name: "dns-provider-selector", + on: { + selectedChange: ({ form, $event }: any) => { + form.dnsProviderAccessType = $event.accessType; + }, + }, + }, + valueChange({ form }: any) { + form.dnsProviderAccessId = null; + }, + }, + }, + dnsProviderAccessType: { + title: t("certd.domain.domainProviderAccessType"), + type: "text", + form: { show: false }, + }, + dnsProviderAccessId: { + title: t("certd.domain.domainProviderAccess"), + type: "text", + form: { + component: { + name: "access-selector", + vModel: "modelValue", + type: compute(({ form }: any) => form.dnsProviderAccessType || form.dnsProviderType), + }, + }, + }, + groupId: { + title: t("certd.fields.group"), + type: "text", + form: { + component: { + name: GroupSelector, + vModel: "modelValue", + type: "site", + }, + }, + }, + }; + + return function openSiteImportTaskDialog(req: { afterSubmit?: (res?: any) => void; form?: any }) { + openFormDialog({ + title: t("certd.domain.importFromProvider"), + columns, + initialForm: { ...req.form }, + onSubmit: async (form: any) => { + const res = await siteInfoApi.ImportTaskSave({ + key: form.key, + dnsProviderType: form.dnsProviderType, + dnsProviderAccessId: form.dnsProviderAccessId, + groupId: form.groupId, + }); + if (req.afterSubmit) { + req.afterSubmit(res); + } + }, + }); + }; +} + +export function useSiteImportTaskManage() { + const { openFormDialog } = useFormDialog(); + const { t } = useI18n(); + const settingStore = useSettingStore(); + return async function openSiteImportTaskManageDialog(req: { + afterSubmit?: (res?: any) => void; + form?: any; + zIndex?: number; + }) { + settingStore.checkPlus(); + await openFormDialog({ + title: t("certd.domain.importFromProvider"), + body: () => , + zIndex: req.zIndex, + onSubmit: async (form: any) => { + if (req.afterSubmit) { + req.afterSubmit(form); + } + }, + }); }; } diff --git a/packages/ui/certd-server/metadata/deploy_CertApply.yaml b/packages/ui/certd-server/metadata/deploy_CertApply.yaml index d61c9dbe2..83476096e 100644 --- a/packages/ui/certd-server/metadata/deploy_CertApply.yaml +++ b/packages/ui/certd-server/metadata/deploy_CertApply.yaml @@ -106,9 +106,13 @@ input: onSelectedChange: ctx.compute(({form})=>{ return ($event)=>{ form.dnsProviderAccessType = $event.accessType - form.dnsProviderAccess = null } - }) + }), + onChange: ctx.compute(({form})=>{ + return ($event)=>{ + form.dnsProviderAccess = null + } + }), }, } diff --git a/packages/ui/certd-server/src/controller/user/monitor/site-info-controller.ts b/packages/ui/certd-server/src/controller/user/monitor/site-info-controller.ts index 8ae3292cc..30b211679 100644 --- a/packages/ui/certd-server/src/controller/user/monitor/site-info-controller.ts +++ b/packages/ui/certd-server/src/controller/user/monitor/site-info-controller.ts @@ -136,6 +136,55 @@ export class SiteInfoController extends CrudController { return this.ok(); } + @Post("/import/save", { description: Constants.per.authOnly, summary: "保存站点证书监控导入任务" }) + async siteInfoImportSave(@Body(ALL) body: any) { + const { projectId, userId } = await this.getProjectUserIdWrite(); + const { dnsProviderType, dnsProviderAccessId, key, groupId } = body; + const item = await this.service.saveSiteInfoImportTask({ + userId: userId, + projectId: projectId, + dnsProviderType, + dnsProviderAccessId, + key, + groupId, + }); + return this.ok(item); + } + + @Post("/import/status", { description: Constants.per.authOnly, summary: "查询站点证书监控导入任务状态" }) + async siteInfoImportStatus() { + const { projectId, userId } = await this.getProjectUserIdRead(); + const task = await this.service.getSiteInfoImportTaskStatus({ + userId: userId, + projectId: projectId, + }); + return this.ok(task); + } + + @Post("/import/delete", { description: Constants.per.authOnly, summary: "删除站点证书监控导入任务" }) + async siteInfoImportDelete(@Body(ALL) body: any) { + const { projectId, userId } = await this.getProjectUserIdWrite(); + const { key } = body; + await this.service.deleteSiteInfoImportTask({ + userId: userId, + projectId: projectId, + key, + }); + return this.ok(); + } + + @Post("/import/start", { description: Constants.per.authOnly, summary: "开始站点证书监控导入任务" }) + async siteInfoImportStart(@Body(ALL) body: any) { + const { projectId, userId } = await this.getProjectUserIdWrite(); + const { key } = body; + await this.service.startSiteInfoImportTask({ + key, + userId: userId, + projectId: projectId, + }); + return this.ok(); + } + @Post("/ipCheckChange", { description: Constants.per.authOnly, summary: "修改IP检查设置" }) async ipCheckChange(@Body(ALL) bean: any) { await this.checkOwner(this.service, bean.id, "read"); diff --git a/packages/ui/certd-server/src/modules/auto/fix/legacy-acme-account-access-fix.ts b/packages/ui/certd-server/src/modules/auto/fix/legacy-acme-account-access-fix.ts index 6dc33b772..8820f7d77 100644 --- a/packages/ui/certd-server/src/modules/auto/fix/legacy-acme-account-access-fix.ts +++ b/packages/ui/certd-server/src/modules/auto/fix/legacy-acme-account-access-fix.ts @@ -98,14 +98,17 @@ export class LegacyAcmeAccountAccessFix { continue; } const name = buildAcmeAccountAccessName(parsedKey.caType, parsedKey.email); - const exists = await this.accessService.findOne({ - where: { + const query = { userId: record.userId, - projectId: record.projectId, type: "acmeAccount", subtype: parsedKey.caType, name, - } as any, + } as any + if (record.projectId) { + query.projectId = record.projectId; + } + const exists = await this.accessService.findOne({ + where:query, }); if (exists) { continue; diff --git a/packages/ui/certd-server/src/modules/mine/service/models.ts b/packages/ui/certd-server/src/modules/mine/service/models.ts index effd64121..c0843f9ed 100644 --- a/packages/ui/certd-server/src/modules/mine/service/models.ts +++ b/packages/ui/certd-server/src/modules/mine/service/models.ts @@ -58,3 +58,10 @@ export class UserDomainImportSetting extends BaseSettings { domainImportList: { dnsProviderType: string; dnsProviderAccessId: number; key: string; title: string; icon?: string }[]; } + +export class UserSiteInfoImportSetting extends BaseSettings { + static __title__ = "用户站点证书监控导入设置"; + static __key__ = "user.siteInfo.import"; + + siteInfoImportList: { dnsProviderType: string; dnsProviderAccessId: number; key: string; title: string; icon?: string; groupId?: number }[]; +} diff --git a/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts b/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts index 7defe118a..d94caec26 100644 --- a/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts +++ b/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts @@ -1,26 +1,30 @@ -import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; -import { BaseService, Constants, isEnterprise, NeedSuiteException, NeedVIPException, SysSettingsService } from "@certd/lib-server"; -import { InjectEntityModel } from "@midwayjs/typeorm"; -import { In, Repository } from "typeorm"; -import { SiteInfoEntity } from "../entity/site-info.js"; -import { siteTester } from "./site-tester.js"; -import dayjs from "dayjs"; -import { logger, utils } from "@certd/basic"; -import { PeerCertificate } from "tls"; -import { NotificationService } from "../../pipeline/service/notification-service.js"; -import { isComm, isPlus } from "@certd/plus-core"; +import { http, logger, utils } from "@certd/basic"; import { UserSuiteService } from "@certd/commercial-core"; -import { UserSettingsService } from "../../mine/service/user-settings-service.js"; -import { UserSiteMonitorSetting } from "../../mine/service/models.js"; -import { SiteIpService } from "./site-ip-service.js"; -import { SiteIpEntity } from "../entity/site-ip.js"; -import { Cron } from "../../cron/cron.js"; -import { dnsContainer } from "./dns-custom.js"; +import { AccessService, BaseService, Constants, isEnterprise, NeedSuiteException, NeedVIPException, SysSettingsService } from "@certd/lib-server"; +import { Pager } from "@certd/pipeline"; +import { createDnsProvider, dnsProviderRegistry, DomainParser } from "@certd/plugin-lib"; +import { isComm, isPlus } from "@certd/plus-core"; +import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import dayjs from "dayjs"; import { merge } from "lodash-es"; -import { JobHistoryService } from "./job-history-service.js"; -import { JobHistoryEntity } from "../entity/job-history.js"; +import { PeerCertificate } from "tls"; +import { In, Repository } from "typeorm"; +import { BackTask, taskExecutor } from "../../basic/service/task-executor.js"; +import { Cron } from "../../cron/cron.js"; +import { UserSiteInfoImportSetting, UserSiteMonitorSetting } from "../../mine/service/models.js"; +import { UserSettingsService } from "../../mine/service/user-settings-service.js"; +import { TaskServiceBuilder } from "../../pipeline/service/getter/task-service-getter.js"; +import { NotificationService } from "../../pipeline/service/notification-service.js"; import { UserService } from "../../sys/authority/service/user-service.js"; import { ProjectService } from "../../sys/enterprise/service/project-service.js"; +import { JobHistoryEntity } from "../entity/job-history.js"; +import { SiteInfoEntity } from "../entity/site-info.js"; +import { SiteIpEntity } from "../entity/site-ip.js"; +import { dnsContainer } from "./dns-custom.js"; +import { JobHistoryService } from "./job-history-service.js"; +import { SiteIpService } from "./site-ip-service.js"; +import { siteTester } from "./site-tester.js"; @Provide() @Scope(ScopeEnum.Request, { allowDowngrade: true }) @@ -51,6 +55,12 @@ export class SiteInfoService extends BaseService { @Inject() projectService: ProjectService; + @Inject() + accessService: AccessService; + + @Inject() + taskServiceBuilder: TaskServiceBuilder; + @Inject() cron: Cron; @@ -64,7 +74,6 @@ export class SiteInfoService extends BaseService { //企业模式不限制 return; } - if (isComm()) { const suiteSetting = await this.userSuiteService.getSuiteSetting(); if (suiteSetting.enabled) { @@ -483,6 +492,219 @@ export class SiteInfoService extends BaseService { await batchAdd(list); } + async startSiteInfoImportTask(req: { userId: number; projectId: number; key: string }) { + const key = req.key; + const setting = await this.userSettingsService.getSetting(req.userId, req.projectId, UserSiteInfoImportSetting); + const item = setting.siteInfoImportList.find(item => item.key === key); + if (!item) { + throw new Error(`站点监控导入任务(${key})还未注册`); + } + const { dnsProviderType, dnsProviderAccessId, title, groupId } = item; + + const TASK_TYPE = "siteInfoImportTask"; + taskExecutor.start( + new BackTask({ + type: TASK_TYPE, + key, + title, + run: async (task: BackTask) => { + await this._syncSitesFromProvider( + { + userId: req.userId, + projectId: req.projectId, + dnsProviderType, + dnsProviderAccessId, + groupId, + }, + task + ); + }, + }) + ); + } + + private async _syncSitesFromProvider(req: { userId: number; projectId: number; dnsProviderType: string; dnsProviderAccessId: number; groupId?: number }, task: BackTask) { + const { userId, projectId, dnsProviderType, dnsProviderAccessId, groupId } = req; + + const serviceGetter = this.taskServiceBuilder.create({ userId, projectId }); + const subDomainGetter = await serviceGetter.getSubDomainsGetter(); + const domainParser = new DomainParser(subDomainGetter); + + const access = await this.accessService.getById(dnsProviderAccessId, userId, projectId); + const context = { access, logger, http, utils, domainParser, serviceGetter }; + const dnsProvider = await createDnsProvider({ dnsProviderType, context }); + + // 1. 先获取主域名列表(每个 domain 翻页) + const domainPager = new Pager({ pageNo: 1, pageSize: 50 }); + const domainList: string[] = []; + while (true) { + const pageRet = await dnsProvider.getDomainListPage(domainPager); + for (const item of pageRet.list || []) { + domainList.push(item.domain); + } + if (!pageRet.list || pageRet.list.length < domainPager.pageSize) { + break; + } + domainPager.pageNo++; + } + + // 2. 根据 provider 是否支持 getRecordListPage 决定处理方式 + const skipTypes = new Set(["TXT", "NS", "SOA", "SRV", "CAA", "PTR"]); + for (const domain of domainList) { + if (!dnsProvider.getRecordListPage) { + // 不支持解析记录列表时,直接把主域名作为一个站点 + try { + await this.add({ + userId, + projectId, + groupId, + domain, + name: domain, + httpsPort: 443, + } as any); + task.incrementCurrent(); + } catch (e) { + if (e.message && e.message.indexOf("已达上限") >= 0) { + task.addError(`${domain}: ${e.message}`); + break; + } + task.incrementSkip(); + } + continue; + } + // 支持 getRecordListPage:翻页获取解析记录,过滤掉泛域名(*.)和不支持的类型 + const recordPager = new Pager({ pageNo: 1, pageSize: 100 }); + while (true) { + const pageRet = await dnsProvider.getRecordListPage(domain, recordPager); + for (const record of pageRet.list || []) { + task.incrementCurrent(); + const typeUpper = (record.type || "").toUpperCase(); + if (skipTypes.has(typeUpper)) { + task.incrementSkip(); + continue; + } + const fullRecord = record.fullRecord; + if (!fullRecord || fullRecord.startsWith("*.") || fullRecord.startsWith("_acme-challenge")) { + task.incrementSkip(); + continue; + } + try { + await this.add({ + userId, + projectId, + groupId, + domain: fullRecord, + name: fullRecord, + httpsPort: 443, + } as any); + } catch (e) { + if (e.message && e.message.indexOf("已达上限") >= 0) { + task.addError(`${fullRecord}: ${e.message}`); + return; + } + task.incrementSkip(); + } + } + if (!pageRet.list || pageRet.list.length < recordPager.pageSize) { + break; + } + recordPager.pageNo++; + } + } + task.setTotal(task.current || task.total || 0); + logger.info(`从域名提供商${dnsProviderType}导入站点完成,共处理${task.current}个记录,跳过${task.getSkipCount()}个,成功${task.getSuccessCount()}个,失败${task.getErrorCount()}个`); + } + + async getSiteInfoImportTaskStatus(req: { userId?: number; projectId?: number }) { + const userId = req.userId || 0; + const projectId = req.projectId; + const setting = await this.userSettingsService.getSetting(userId, projectId, UserSiteInfoImportSetting); + const list = setting?.siteInfoImportList || []; + const TASK_TYPE = "siteInfoImportTask"; + const taskList: any = []; + for (const item of list) { + const { key } = item; + const task = taskExecutor.get(TASK_TYPE, key); + taskList.push({ ...item, task }); + } + return taskList; + } + + async getSiteInfoImportProviderTitle(req: { userId?: number; projectId?: number; dnsProviderType: string; dnsProviderAccessId: number }) { + const userId = req.userId || 0; + const projectId = req.projectId; + const { dnsProviderType, dnsProviderAccessId } = req; + const dnsProviderDefine = dnsProviderRegistry.getDefine(dnsProviderType); + if (!dnsProviderDefine) { + throw new Error(`该域名提供商(${dnsProviderType})不存在,请检查是否已被注册`); + } + const access = await this.accessService.getSimpleInfo(dnsProviderAccessId); + if (!access || access.userId !== userId) { + throw new Error(`该授权(${dnsProviderAccessId})不存在,请检查是否已被删除`); + } + if (projectId && access.projectId !== projectId) { + throw new Error(`该授权(${dnsProviderAccessId})不存在,请检查是否已被删除`); + } + return { + title: `${dnsProviderDefine.title}_${access.name || ""}`, + icon: dnsProviderDefine.icon || "", + }; + } + + async addSiteInfoImportTask(req: { userId?: number; projectId?: number; dnsProviderType: string; dnsProviderAccessId: number; index?: number; groupId?: number }) { + const userId = req.userId || 0; + const projectId = req.projectId; + const { dnsProviderType, dnsProviderAccessId, index = 0, groupId } = req; + const key = `user_${userId}_${dnsProviderType}_${dnsProviderAccessId}`; + const { title, icon } = await this.getSiteInfoImportProviderTitle(req); + const setting = await this.userSettingsService.getSetting(userId, projectId, UserSiteInfoImportSetting); + setting.siteInfoImportList = setting.siteInfoImportList || []; + if (setting.siteInfoImportList.find(item => item.key === key)) { + throw new Error(`该站点监控导入任务${key}已存在`); + } + const access = await this.accessService.getAccessById(dnsProviderAccessId, true, userId, projectId); + if (!access) { + throw new Error(`该授权(${dnsProviderAccessId})不存在,请检查是否已被删除`); + } + const item = { dnsProviderType, dnsProviderAccessId, key, title, icon: icon || "", groupId }; + setting.siteInfoImportList.splice(index, 0, item); + await this.userSettingsService.saveSetting(userId, projectId, setting); + return item; + } + + async deleteSiteInfoImportTask(req: { userId?: number; projectId?: number; key: string }) { + const userId = req.userId || 0; + const projectId = req.projectId; + const { key } = req; + const setting = await this.userSettingsService.getSetting(userId, projectId, UserSiteInfoImportSetting); + setting.siteInfoImportList = setting.siteInfoImportList || []; + const index = setting.siteInfoImportList.findIndex(item => item.key === key); + if (index === -1) { + throw new Error(`该站点监控导入任务${key}不存在`); + } + setting.siteInfoImportList.splice(index, 1); + const TASK_TYPE = "siteInfoImportTask"; + taskExecutor.clear(TASK_TYPE, key); + await this.userSettingsService.saveSetting(userId, projectId, setting); + } + + async saveSiteInfoImportTask(req: { userId?: number; projectId?: number; dnsProviderType: string; dnsProviderAccessId: number; key?: string; groupId?: number }) { + const userId = req.userId || 0; + const projectId = req.projectId; + const { dnsProviderType, dnsProviderAccessId, key, groupId } = req; + const setting = await this.userSettingsService.getSetting(userId, projectId, UserSiteInfoImportSetting); + setting.siteInfoImportList = setting.siteInfoImportList || []; + let index = 0; + if (key) { + index = setting.siteInfoImportList.findIndex(item => item.key === key); + if (index === -1) { + throw new Error(`该站点监控导入任务${key}不存在`); + } + await this.deleteSiteInfoImportTask({ userId, projectId, key }); + } + return await this.addSiteInfoImportTask({ userId, projectId, dnsProviderType, dnsProviderAccessId, index, groupId }); + } + clearSiteMonitorJob(userId: number, projectId?: number) { this.cron.remove(`siteMonitor_${userId}_${projectId || ""}`); } diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/dns-provider/aliyun-dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/dns-provider/aliyun-dns-provider.ts index e1e6a2c9e..39721031d 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/dns-provider/aliyun-dns-provider.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/dns-provider/aliyun-dns-provider.ts @@ -1,4 +1,4 @@ -import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; +import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, DnsResolveRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; import { AliyunAccess } from "../../plugin-lib/aliyun/access/aliyun-access.js"; import { AliyunClient } from "../../plugin-lib/aliyun/index.js"; import { Pager, PageRes, PageSearch } from "@certd/pipeline"; @@ -177,6 +177,35 @@ export class AliyunDnsProvider extends AbstractDnsProvider { total: ret.TotalCount, }; } + + async getRecordListPage(domain: string, req: PageSearch): Promise> { + const pager = new Pager(req); + const params = { + RegionId: "cn-hangzhou", + DomainName: domain, + PageSize: pager.pageSize, + PageNumber: pager.pageNo, + }; + + const requestOption = { + method: "POST", + }; + + const ret = await this.client.request("DescribeDomainRecords", params, requestOption); + const rawList = ret.DomainRecords?.Record || []; + const list = rawList.map(item => ({ + id: item.RecordId, + hostRecord: item.RR, + fullRecord: item.RR === "@" ? domain : `${item.RR}.${domain}`, + type: item.Type, + value: item.Value, + })); + + return { + list, + total: ret.TotalCount, + }; + } } new AliyunDnsProvider(); diff --git a/packages/ui/certd-server/src/plugins/plugin-cloudflare/dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-cloudflare/dns-provider.ts index d48868da2..3943fc610 100644 --- a/packages/ui/certd-server/src/plugins/plugin-cloudflare/dns-provider.ts +++ b/packages/ui/certd-server/src/plugins/plugin-cloudflare/dns-provider.ts @@ -1,4 +1,4 @@ -import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; +import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, DnsResolveRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; import { CloudflareAccess } from "./access.js"; import { Pager, PageRes, PageSearch } from "@certd/pipeline"; @@ -137,6 +137,34 @@ export class CloudflareDnsProvider extends AbstractDnsProvider list, }; } + + async getRecordListPage(domain: string, req: PageSearch): Promise> { + const pager = new Pager(req); + + const zoneId = await this.getZoneId(domain); + let url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?page=${pager.pageNo}&per_page=${pager.pageSize}`; + if (req.searchKey) { + url += `&name=${req.searchKey}`; + } + const ret = await this.access.doRequestApi(url, null, "get"); + + let list = ret.result || []; + list = list.map((item: any) => { + const hostRecord = item.name === domain ? "@" : item.name.slice(0, item.name.length - domain.length - 1); + return { + id: item.id, + hostRecord, + fullRecord: item.name, + type: item.type, + value: item.content, + }; + }); + const total = ret.result_info.total_count || list.length; + return { + total, + list, + }; + } } //实例化这个provider,将其自动注册到系统中 diff --git a/packages/ui/certd-server/src/plugins/plugin-hipmdnsmgr/dns-provider/hipmdnsmgr-dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-hipmdnsmgr/dns-provider/hipmdnsmgr-dns-provider.ts index 69abf383c..0071db1bc 100644 --- a/packages/ui/certd-server/src/plugins/plugin-hipmdnsmgr/dns-provider/hipmdnsmgr-dns-provider.ts +++ b/packages/ui/certd-server/src/plugins/plugin-hipmdnsmgr/dns-provider/hipmdnsmgr-dns-provider.ts @@ -30,16 +30,6 @@ export class HipmDnsmgrDnsProvider extends AbstractDnsProvider<{ domainId: strin // 1. 获取域名 ID(双层查询策略) const domainId = await this.access.getDomainId(domain); this.logger.debug('[HiPM DNSMgr] 找到域名:', domain, 'ID:', domainId); - // 1. 获取域名列表,找到对应的域名 ID - const domainList = await this.access.getDomainList(); - const domainInfo = domainList.find((item: any) => item.domain === domain); - - if (!domainInfo) { - throw new Error(`[HiPM DNSMgr] 未找到域名:${domain}`); - } - - const domainId = String(domainInfo.id); - this.logger.debug("[HiPM DNSMgr] 找到域名:", domain, "ID:", domainId); // 2. 创建 DNS 记录 const name = hostRecord; // 使用子域名,如 _acme-challenge diff --git a/packages/ui/certd-server/src/plugins/plugin-tencent/dns-provider/tencent-dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-tencent/dns-provider/tencent-dns-provider.ts index fb826919f..07d50744f 100644 --- a/packages/ui/certd-server/src/plugins/plugin-tencent/dns-provider/tencent-dns-provider.ts +++ b/packages/ui/certd-server/src/plugins/plugin-tencent/dns-provider/tencent-dns-provider.ts @@ -1,4 +1,4 @@ -import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; +import { AbstractDnsProvider, CreateRecordOptions, DnsResolveRecord, DomainRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; import { TencentAccess } from "../../plugin-lib/tencent/index.js"; import { Pager, PageRes, PageSearch } from "@certd/pipeline"; @@ -114,5 +114,29 @@ export class TencentDnsProvider extends AbstractDnsProvider { const total = ret.DomainCountInfo?.AllTotal || list.length; return { total, list }; } + + async getRecordListPage(domain: string, req: PageSearch): Promise> { + const pager = new Pager(req); + + const params: any = { + Domain: domain, + Offset: pager.getOffset(), + Limit: pager.pageSize, + }; + if (req.searchKey) { + params.Subdomain = req.searchKey; + } + const ret = await this.client.DescribeRecordList(params); + let list = ret.RecordList || []; + list = list.map((item: any) => ({ + id: String(item.RecordId), + hostRecord: item.Name, + fullRecord: item.Name === "@" ? domain : `${item.Name}.${domain}`, + type: item.Type, + value: item.Value, + })); + const total = ret.TotalCount || list.length; + return { total, list }; + } } new TencentDnsProvider(); diff --git a/trigger/build.trigger b/trigger/build.trigger index 57d6f91b7..e2c91fe7d 100644 --- a/trigger/build.trigger +++ b/trigger/build.trigger @@ -1 +1 @@ -02:38 +23:31