Compare commits

...
79 Commits
Author SHA1 Message Date
xiaojunnuo 14809127d6 v1.44.4 2026-09-12 19:23:35 +08:00
xiaojunnuo bd8b251d6e build: prepare to build 2026-09-12 19:17:14 +08:00
xiaojunnuo 5563abb13a fix: 修复证书插件配置无法打开的bug 2026-09-12 19:13:23 +08:00
xiaojunnuo 51e83e4c38 fix: 修复添加步骤会修改所有步骤标题的bug 2026-09-12 18:51:53 +08:00
xiaojunnuo 2b1a5296fe chore(certd-client): fix step form save data overwritten issue and add no payment tip
1. 修复步骤表单保存时因复用reactive对象导致已保存步骤被覆盖的问题,深拷贝currentStep后再传出
2. 新增无支付方式时显示暂不支持在线支付的提示
2026-09-12 18:49:24 +08:00
xiaojunnuo bded368cbb perf: 新增余额管理,管理员充值功能
1. 新增余额管理页面,支持用户余额查询与充值
2. 新增余额变更记录页面,查看用户余额明细
3. 补充多语言国际化文案
4. 修复免费订单显示文案为0元而非免费
5. 隐藏审核页面的操作按钮
6. 注释掉无用的堆日志启动代码
2026-09-12 16:06:53 +08:00
xiaojunnuo 6a649aaca0 perf(acme-account-access): AcmeAccount授权允许修改eab,重新生成account
修改了三处ACME账号表单的禁用判断条件,新增判断当editAccount为true时不禁用控件,允许在编辑已有账号时修改相关配置
2026-09-12 14:44:43 +08:00
xiaojunnuo e971145686 perf(ui): 商业版套餐购买不要求设置支付方式
1. 将套餐设置页面的"需要开启支付方式"提示改为"建议"
2. 新增支付方式为空且余额不足时的红色警告提示
3. 默认开启返利余额使用并修复支付字典加载异常处理
2026-09-12 14:28:52 +08:00
xiaojunnuo bbca449f94 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-09-12 09:32:36 +08:00
xiaojunnuo e6852c2715 docs: 更新AGENTS.md编码规范说明
refactor(plugin-fastly): 简化多行API调用代码
refactor(plugin-serv
2026-09-11 19:01:29 +08:00
xiaojunnuo 47cb523307 chore(cloudflare): 优化权限说明的换行展示
修复Cloudflare插件的权限说明文本排版,将权限项用换行分开展示,提升可读性
2026-09-10 15:13:38 +08:00
xiaojunnuo cbf46b5c48 build: release 2026-09-07 00:07:49 +08:00
xiaojunnuo 7ed3105d97 build: publish 2026-09-06 23:51:23 +08:00
xiaojunnuo 61b945c08f build: trigger build image 2026-09-06 23:51:13 +08:00
xiaojunnuo cc60cd4cd1 v1.44.3 2026-09-06 23:50:28 +08:00
xiaojunnuo dade599acb build: prepare to build 2026-09-06 23:46:14 +08:00
xiaojunnuo 1070417a36 chore: 修复fastly 通配符型号转换格式的问题 2026-09-06 23:45:56 +08:00
xiaojunnuo 81f676f1ca build: prepare to build 2026-09-06 23:29:25 +08:00
xiaojunnuo 0cf0608d3f build: prepare to build 2026-09-06 23:25:52 +08:00
xiaojunnuo dac94b64f0 fix(plugin-service): 修复同步自己开发的云端插件不显示已安装状态问题
将开发者ID、VIP状态、依赖插件、AI检查状态、插件状态、应用ID和作者字段加入插件更新逻辑,完善插件信息同步的完整性
2026-09-06 23:08:31 +08:00
xiaojunnuo 8c88092235 docs: 优化docker镜像文档的排版和内容
调整了README和docker安装指南中的镜像说明部分,将原有的零散说明改为表格形式统一展示,修正了镜像地址的展示位置,补充了稳定版slim/armv7的完整镜像地址信息,清理了重复的说明内容。
2026-09-06 17:28:00 +08:00
Steven ZhuandClaude Sonnet 5 2a2e963914 perf(plugin): add Fastly custom TLS certificates integration (#797) @szhu25
* feat(plugin): add Fastly custom TLS certificates integration

Introduces a complete set of Fastly plugins for automating custom TLS certificates deployment:
- `FastlyAccess`: Handles Fastly API authentication and provides helpers for dynamic data fetching (Services, Domains, Configs).
- `FastlyUploadCert`: Uploads a new private key and certificate (2-step POST process) or updates an existing one (PATCH).
- `FastlyDeployCert`: Activates a custom TLS certificate by linking it to a TLS configuration and domain via TLS Activations API.
- `FastlyRefreshCert`: Bulk updates existing custom TLS certificates.
- `FastlyPurgeCache`: Purges all cache for a specified Fastly Service.

Includes comprehensive unit tests covering API request formats and error handling for all tasks.

* fix(fastly): drop invalid 'multiple' option from refresh-cert input define

createRemoteSelectInputDefine has no 'multiple' option; multi-select is
already its default (mode: "tags", single: false). The excess property
broke the production build (TS2353).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015dRf6EbAmwb37cpnvgBDrh

* fix(fastly): correct TLS list unwrapping, paginate, harden purge

- access: ctx.http already unwraps to the response body, so the JSON:API
  item array is at body.data, not body.data.data — getTlsConfigurations /
  getTlsDomains / getCertificates always returned [] and the selectors
  were empty. Fetch via listAllJsonApi, which also pages past Fastly's
  20-item default so accounts with more than one page are fully listed.
- purge-cache: send no body to /purge_all (auth is header-only) instead
  of an empty JSON:API object.
- upload-cert / refresh-cert: note in the helper text that Fastly requires
  the renewed cert to reuse the original private key for PATCH updates.
- test: cover the list-helper unwrapping, pagination aggregation, and the
  bare-array /service response.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015dRf6EbAmwb37cpnvgBDrh

* fix(fastly): use output-selector for deploy cert ID input

The FastlyDeployCert `certificateId` input was a free-text box whose
helper pointed users at a non-existent "advanced settings" workflow to
grab the upstream output variable. certd only surfaces a prior step's
@TaskOutput through an input whose component is `output-selector`, so the
FastlyUploadCert `fastlyCertId` output was never selectable.

Bind the input to `output-selector` with `from: ["FastlyUploadCert"]`,
matching the pattern used by plugin-aliyun deploy-to-cdn.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J

* fix(fastly): accept cert or uploaded id in deploy, share upload helpers

Rework FastlyDeployCert to mirror the AWS CloudFront pattern instead of
demanding a hand-typed / separately-uploaded certificate id:

- The `certificateId` text box (and the earlier FastlyUploadCert-only
  output-selector) is replaced by a `cert: CertInfo | string` input whose
  output-selector accepts both a cert-apply step's cert and a
  FastlyUploadCert step's `fastlyCertId`.
- execute() binds the id directly when it got a string; when it got a raw
  CertInfo it uploads to Fastly first (new optional `name` input), then
  creates the TLS activation. So a plain "apply cert -> activate" pipeline
  needs no separate upload step.

Extract the Fastly certificate create (2-step key + cert) and update
(PATCH) flows into FastlyAccess.createCertificate / updateCertificate and
reuse them from both FastlyUploadCert and FastlyDeployCert.

Tests updated to drive the plugins through a real FastlyAccess with only
doRequestApi stubbed; added a deploy case for the raw-CertInfo path.

Note: build/tests not run locally (no Node toolchain on this machine) —
needs CI / local verification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J

* fix(fastly): reuse existing key/cert when Fastly reports "already exists"

Fastly deduplicates TLS private keys (and byte-identical certificates) by
content, so re-running an upload fails with 400 "... already exists:
'<id>'". Recover that id and carry on instead of aborting:

- ensurePrivateKey(): on "already exists", parse the id from the error
  (or match an existing key by name), else throw actionable guidance.
- createCertificate(): same treatment for the /tls/certificates POST;
  when no id is recoverable, tell the user to supply the certificate id
  so the PATCH (update) path is used.
- Shared FastlyAccess.extractExistingId() helper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J

* fix(fastly): per-domain TLS activation, idempotent on renewal

The TLS 域名 / TLS 配置 selectors use createRemoteSelectInputDefine, which
defaults to multi-select ("tags"), so domainId arrived as an array and was
written straight into tls_domain.data.id -> Fastly 500. A tls_activation
also binds exactly one domain, so wildcard + apex needs one each.

- domainId -> domainIds (string[]); loop, one activation per domain.
  configurationId gets single:true; both inputs are still normalised in
  execute() (array-or-scalar, plus legacy `domainId` key).
- New FastlyAccess.deployActivation(): looks up an existing activation for
  the domain+configuration and PATCHes it onto the new certificate;
  otherwise POSTs a fresh one. Renewals no longer fail on a duplicate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J

* feat(fastly): list service domains for TLS activation, not cert SANs

The TLS 域名 selector was populated from GET /tls/domains, which only
echoes certificate SAN entries — so a wildcard cert offered "*.g0l.net",
which cannot be activated unless a service actually serves that wildcard.

onGetTlsDomainList now lists the domains configured on the active version
of each Fastly service (new FastlyAccess.getServiceDomains()), which are
the names you can actually activate (e.g. www.g0l.net, covered by the
wildcard). Cert-derived TLS domains are still appended, tagged "(来自证书)".
The field stays free-entry so an unlisted host can be typed in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:20:14 +08:00
Steven ZhuandClaude Sonnet 5 1b882a2ca4 AWS ratelimit fix (#796) @szhu25
* Parse PEM chain and import certificate chain

Split the PEM in certInfo.crt into a leaf certificate and intermediate chain (using a lookbehind regex), trim the blocks, and pass the chain to ImportCertificateCommand only when present. Replace console.log with this.logger.info and log the returned CertificateArn. This ensures the leaf cert is uploaded separately from its chain and avoids sending an empty CertificateChain.

* Add AWS retry & CloudFront deployment wait

Introduce robust retry and polling helpers to handle AWS throttling and CloudFront propagation. Added AwsClient.withRetry (exponential backoff, handles common throttling errors, default 5 attempts/base 2s) and waitForDistributionDeployed (polls until distribution Status is "Deployed", default 10min timeout/15s interval). Update deploy-to-cloudfront plugin to use withRetry for Get/UpdateDistribution and importCertificate, pass AwsClient into uploadToACM, and wait for each distribution to finish deploying before continuing to avoid PreconditionFailed errors. Improves reliability when facing rate limits and global CloudFront propagation delays; adds informative logging for retry and deployment status.

* fix(aws): resolve rate limit and runtime import bugs

* fix(aws): broaden retry detection and harden CloudFront poll

- withRetry: retry on Route53 PriorRequestNotComplete, SlowDown, SDK v3
  $retryable.throttling hints and HTTP 429/503; extract detection into
  isRetryableAwsError
- withRetry: add equal jitter to backoff so concurrent pipelines don't
  retry in lockstep
- waitForDistributionDeployed: a throttle that outlives withRetry's budget
  keeps polling until the deadline instead of aborting the deploy
- test: cover withRetry retry/exhaustion/non-throttle paths

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015dRf6EbAmwb37cpnvgBDrh

* fix(aws): fix build break and make withRetry the sole retry authority

- withRetry<T = any>: default the type param so call sites passing an
  untyped SDK client (@aws-sdk/* is a lazyDependency with no build-time
  types) don't collapse the result to `unknown`. Also annotate the two
  result consumers as `any`. Fixes the TS2339 errors that broke the
  Docker build (aws-client.ts, plugin-deploy-to-cloudfront.ts).
- Set maxAttempts: 1 on every AWS SDK client (ACM/STS/Route53/CloudFront)
  so the SDK's own retry strategy no longer stacks on top of withRetry.
- isRetryableAwsError: broaden to cover what the SDK's `standard` strategy
  used to handle now that it's disabled - $retryable hint, HTTP 429 + any
  5xx, connection-level errors (ECONNRESET/ETIMEDOUT/EAI_AGAIN/...).
- waitForDistributionDeployed: a transient error that outlives withRetry's
  budget now retries on the next poll instead of aborting the whole deploy.
- test: cover 5xx, $retryable, and connection-error retry paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrMVmQyvQjes6t4DGjuBaD

* feat(aws): make CloudFront deploy-wait opt-in (default off)

Polling GetDistribution until Status=Deployed adds 3-8min of pipeline
wall-time per run for no functional gain - CloudFront finishes the
rollout on its own once UpdateDistribution returns 200. Gate it behind
a new "等待部署完成" switch, default false. Retry/backoff on the actual
mutating calls (GetDistributionConfig / UpdateDistribution / ACM import
/ Route53) is unaffected - it wraps those sends directly, not the poll.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrMVmQyvQjes6t4DGjuBaD

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:19:47 +08:00
xiaojunnuo bd9433478a fix(plugin): 修复插件配置保存与加载的相关问题
1.  修复插件配置保存时使用fullName或name作为标识的问题
2.  为证书申请插件配置补充pluginType字段
3.  新增插件配置全局加载逻辑,启动时同步数据库插件配置到内存注册表
4.  优化配置编辑器默认值与选项添加交互
5.  补充vue依赖导入与上下文注入
2026-09-05 00:27:26 +08:00
xiaojunnuo 32ade8e4a3 build: release 2026-09-03 22:16:49 +08:00
xiaojunnuo f2bcdeeb33 build: publish 2026-09-03 22:05:17 +08:00
xiaojunnuo 4099af7f5d build: trigger build image 2026-09-03 22:05:07 +08:00
xiaojunnuo 67f3daae62 v1.44.2 2026-09-03 22:04:19 +08:00
xiaojunnuo c72fa68e4c build: prepare to build 2026-09-03 21:57:56 +08:00
xiaojunnuo 1f5cda452c test(cert): 修复DNS持久化记录服务测试用例的查询方法适配问题
将测试中使用的findOneBy替换为标准的findOne方法,适配服务层实际的查询逻辑,修正测试用例的模拟实现
2026-09-03 21:57:47 +08:00
xiaojunnuo 5a19d2513d build: prepare to build 2026-09-03 21:49:35 +08:00
xiaojunnuo c0f8145615 test(plugin-service): 完善插件服务测试用例
修复并补充插件安装相关的单元测试逻辑,新增findOne仓库方法模拟,校验查询参数正确性,调整断言逻辑以匹配实际的查询次数
2026-09-03 21:49:15 +08:00
xiaojunnuo 2ad14b82fa chore(base-service): 修复findOneBy方法调用不兼容的问题
将弃用的findOneBy替换为标准的findOne查询方式,兼容新版本的TypeORM接口
2026-09-03 21:37:52 +08:00
xiaojunnuo c8bdcdc778 build: prepare to build 2026-09-03 21:20:43 +08:00
xiaojunnuo 15d2bd8484 fix: 修复插件市场插件安装无效的bug 2026-09-03 21:20:17 +08:00
xiaojunnuo 7d4311115c chore(plugin-service): 补全内置插件的installed字段赋值逻辑
为内置插件添加默认的installed状态设置,统一插件安装状态的处理逻辑,修复部分场景下插件安装状态不准确的问题
2026-09-03 01:39:22 +08:00
xiaojunnuo 3efa0582ef Merge branch 'v2-dev' into v2 2026-09-03 01:17:35 +08:00
xiaojunnuo 7624babc99 build: release 2026-09-03 01:17:10 +08:00
xiaojunnuo 6cf8f350f5 build: publish 2026-09-03 00:58:57 +08:00
xiaojunnuo 0e08555220 build: trigger build image 2026-09-03 00:58:47 +08:00
xiaojunnuo fe738fa7d4 v1.44.1 2026-09-03 00:50:48 +08:00
xiaojunnuo 2439f4ab02 build: prepare to build 2026-09-03 00:44:44 +08:00
xiaojunnuo 75b2b146ce test: 优化两个测试文件的代码结构与测试逻辑
1.  为证书信息服务测试提取复用的服务创建函数,减少重复代码
2.  修复插件服务测试用例,修正作者校验逻辑、同步逻辑、插件导入更新等多处测试断言
2026-09-03 00:43:53 +08:00
xiaojunnuo 1f3c096517 build: prepare to build 2026-09-03 00:22:26 +08:00
xiaojunnuo d264388c00 build: prepare to build 2026-09-03 00:17:04 +08:00
xiaojunnuo b8eaeeaa64 refactor(plugin-service): 调整在线插件作者请求的必填字段
将name和email从必填改为可选字段,优化接口参数灵活性
2026-09-03 00:16:59 +08:00
xiaojunnuo dba1a486cb chore(ui/certd-server): update metadata config and fix event name
1. 修正deploy_CertApply.yaml中DNS持久验证的说明文本
2. 将表单的selected-change事件名改为驼峰式selectedChange
3. 为acmeAccount的选择组件添加emitImmediate配置
2026-09-03 00:15:49 +08:00
xiaojunnuo 5408ae6ad8 build: prepare to build 2026-09-03 00:08:37 +08:00
Atirna f5430a8925 fix: preserve Route53 TXT record values (#791) 2026-09-02 23:59:45 +08:00
xiaojunnuo 9cc21204b1 docs(install/docker): update mirror table and add aliyun registry tags
1. 补全docker镜像表格的分隔线对齐格式
2. 在release镜像工作流中新增阿里云certd镜像仓库的推送标签
2026-09-02 22:42:25 +08:00
xiaojunnuo f911e06a69 fix: 修复大量问题
1.  更新多处文案,将"仅Let's Encrypt"改为"仅Let's Encrypt 测试环境"
2.  新增getSiteId方法到设置store
3.  优化项目store缓存key和变量声明顺序
4.  重构插件作者字段组件:添加作者注册状态管理、禁用输入框、支持修改邮箱
5.  暴露updatePluginAuthorEmail方法并完善插件类型注释
6.  调整首页统计卡片布局位置
7.  移除多余空行和注释掉Addon插件类型选项
2026-09-01 23:21:06 +08:00
xiaojunnuo 4a6eab0ce6 refactor(plugin): 调整插件相关代码结构与逻辑
1. 为createRemoteSelectInputDefine新增emitImmediate参数
2. 新增BaseService.updateById方法简化更新操作
3. 移除插件服务中多余的YAML序列化工具方法,改为前端处理
4. 优化插件更新、同步逻辑,替换部分update调用为updateById
5. 简化插件作者校验逻辑
2026-09-01 20:53:50 +08:00
xiaojunnuo 1d90fadd0c Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-09-01 00:23:56 +08:00
xiaojunnuo 1b1a5ec94d refactor(dashboard): 重构统计逻辑,优化前端展示与代码结构
1.  重构用户统计数据模型与累加逻辑,使用lodash简化字段操作
2.  调整仪表盘布局与统计卡片样式,适配暗色模式
3.  优化插件系统默认值加载时机,修复配置同步问题
4.  简化多语言文案,统一统计数据展示格式
5.  清理冗余代码与注释,修复代码规范问题
2026-09-01 00:23:53 +08:00
xiaojunnuo d67379f297 fix: 修复编辑证书申请任务导致清空ACME账号的bug
这一版本新增了用户证书生成统计、流水线运行统计功能,优化了插件配置获取和更新流程:
1.  新增用户统计设置模型,统计单域名/多域名/泛域名证书生成数和流水线运行次数
2.  仪表盘新增累计证书数、分类统计和节省金额估算卡片
3.  重构插件配置相关接口,统一插件获取、配置保存逻辑
4.  修复远程选择组件初始值触发逻辑,新增emitImmediate配置项
5.  优化ACME账号切换逻辑,修复对象展开语法语法问题
6.  补充多语言国际化文案,调整部分前端样式细节
2026-08-31 22:54:28 +08:00
xiaojunnuo 249b98df66 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-08-31 13:23:47 +08:00
xiaojunnuo aebbb5cc7d docs: 更新DNS持久验证的说明,移除测试环境限制
将多处提及的"仅Let's Encrypt测试环境可以申请"修改为正式环境支持,同步更新所有相关提示文案
2026-08-31 13:23:40 +08:00
xiaojunnuo 4f020aa8a5 chore: 移除compile脚本中多余的rimraf前置步骤
统一移除多个package.json里compile命令前冗余的rimraf dist && 前缀,简化编译脚本命令
2026-08-30 10:56:42 +08:00
xiaojunnuo 02426c507a docs: update database install doc and docker compose config
1. 修正文档中PG的全称表述并补充执行SQL的前置条件
2. 新增PostgreSQL SSL连接配置注释到docker-compose示例
3. 更新自动升级注释说明支持stable版本
2026-08-30 10:51:06 +08:00
xiaojunnuo ffb7ae79eb Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-08-30 01:52:31 +08:00
xiaojunnuo f58f2f3cd6 fix: 修复旧流水线保存时清空证书仓库证书内容的bug 2026-08-30 01:52:25 +08:00
xiaojunnuo 794abc3d7a Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-08-30 00:43:53 +08:00
xiaojunnuo da406014ad chore(plugin): add online plugin author email update function
1. 新增修改在线插件作者邮箱的接口与前后端实现
2. 调整插件作者相关类型定义,精简冗余字段并添加email字段
3. 新增邮箱格式校验逻辑
4. 在插件发布页面添加修改作者邮箱的交互功能
2026-08-30 00:43:47 +08:00
xiaojunnuo 9842a24a50 perf(dashboard): 新增仪表盘站点监控和域名管理的统计卡片
1. 新增仪表盘站点监控和域名管理的统计卡片,包含总数、异常/过期状态统计
2. 补充多语言国际化文案(zh-CN和en-US)
3. 后端接口新增站点和域名的统计数据查询逻辑
4. 优化统计卡片样式,根据状态显示不同色调
5. 修复代码格式问题和冗余代码
2026-08-30 00:23:21 +08:00
xiaojunnuo fb21ec04a6 chore(scripts): 为release接口url添加v前缀
修正获取指定版本发布信息的接口地址,补上版本号前的v前缀,确保接口调用正确
2026-08-29 15:24:12 +08:00
xiaojunnuo 15b74207dd ci(github actions): 替换npm脚本为自定义node脚本
将原来通过npm调用的设置版本稳定状态的步骤,替换为直接调用自定义的node脚本,简化CI流程并移除额外的npm依赖安装步骤
2026-08-29 15:20:52 +08:00
xiaojunnuo 6055e9b6ba ci(github actions): 优化稳定版发布的npm安装步骤
仅安装指定版本的axios并跳过包锁和保存,减少不必要的依赖安装
2026-08-29 15:12:00 +08:00
xiaojunnuo df3e0814c4 chore: 移除package.json里的root本地链接依赖并调整CI安装步骤
1.  从package.json中删除了无用的root本地链接依赖
2.  简化CI步骤,移除全局安装axios的命令,直接执行npm install
2026-08-29 15:05:25 +08:00
xiaojunnuo 81f660dc49 ci(github actions): 为稳定发布工作流全局安装axios
修复发布流程中axios依赖缺失导致的执行失败问题
2026-08-29 15:03:13 +08:00
xiaojunnuo f56e2a07c6 ci(github-actions): 为稳定发布工作流添加npm安装步骤
在stable-release.yml的发布任务中新增npm install命令,确保执行发布脚本前依赖已正确安装
2026-08-29 14:59:44 +08:00
xiaojunnuo a2fce6cf0b ci(github actions): 更新Docker发布配置
1. 更换Docker Hub登录的密钥名称
2. 为buildx构建添加--provenance和--sbom参数关闭物料清单生成
2026-08-29 12:08:51 +08:00
xiaojunnuo ccb5356bfa ci(ci): 移除稳定版镜像的版本标签构建
更新文档以适配不再发布带版本后缀的稳定版镜像
2026-08-29 12:01:40 +08:00
xiaojunnuo f44084dfd2 chore(base-service): 修复更新方法未校验id和新增重复id问题
1. 在新增逻辑中删除param的id字段避免重复插入
2. 格式化更新方法的id非空校验代码块
2026-08-29 04:22:13 +08:00
xiaojunnuo 948d76f98f build: release 2026-08-29 04:15:09 +08:00
xiaojunnuo 6606f32678 fix(pipeline-controller): 处理非编辑场景下保留id的问题
在非编辑流程中删除请求参数里的id字段,避免意外更新已有数据
2026-08-29 03:52:54 +08:00
xiaojunnuo 6af12d49fc fix(pipeline): 修正流水线保存时的ID判断逻辑并增加参数校验
调整了流水线新增的判断条件,将原来的bean.id <=0改为bean.id ==0或null,同时增加了当bean.id小于0时抛出错误的参数校验逻辑,避免非法ID传入
2026-08-29 03:47:55 +08:00
xiaojunnuo 9decb49d04 refactor: 重构插件同步逻辑,优化插件数据处理流程
1.  调整插件同步时的字段处理逻辑,减少冗余数据存储
2.  提前查询已有插件数据,避免重复创建和更新
3.  拆分插件同步代码结构,提升可读性和可维护性
4.  注释掉下载次数同步逻辑,暂时关闭该功能
5.  简化插件编辑接口的字段处理逻辑
6.  移除插件列表页的复制按钮,新增插件导出功能
7.  更新文档和镜像配置,修正Docker Hub镜像地址
8.  调整文档章节顺序,新增版本号徽章展示
2026-08-29 02:48:56 +08:00
xiaojunnuo 69466c1c41 build: publish 2026-08-29 00:46:02 +08:00
xiaojunnuo adf5163a79 build: trigger build image 2026-08-29 00:45:51 +08:00
124 changed files with 4077 additions and 776 deletions
+21 -15
View File
@@ -92,6 +92,8 @@ jobs:
tags: |
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}
registry.cn-shenzhen.aliyuncs.com/certd/certd:latest
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}
greper/certd:latest
greper/certd:${{steps.get_certd_version.outputs.result}}
ghcr.io/${{ github.repository }}:latest
@@ -111,8 +113,8 @@ jobs:
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-slim
registry.cn-shenzhen.aliyuncs.com/certd/certd:slim
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-slim
certd/certd:slim
certd/certd:${{steps.get_certd_version.outputs.result}}-slim
greper/certd:slim
greper/certd:${{steps.get_certd_version.outputs.result}}-slim
ghcr.io/${{ github.repository }}:slim
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-slim
@@ -129,22 +131,26 @@ jobs:
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7
registry.cn-shenzhen.aliyuncs.com/certd/certd:armv7
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-armv7
certd/certd:armv7
certd/certd:${{steps.get_certd_version.outputs.result}}-armv7
greper/certd:armv7
greper/certd:${{steps.get_certd_version.outputs.result}}-armv7
ghcr.io/${{ github.repository }}:armv7
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-armv7
# - name: Build agent
# uses: docker/build-push-action@v6
# with:
# platforms: linux/amd64,linux/arm64
# push: true
# context: ./packages/ui/agent/
# tags: |
# registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:latest
# registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:${{steps.get_certd_version.outputs.result}}
# greper/certd-agent:latest
# greper/certd-agent:${{steps.get_certd_version.outputs.result}}
- name: Login to Certd Docker Hub account
uses: docker/login-action@v3
with:
username: ${{ secrets.dockerhub_username2 }}
password: ${{ secrets.dockerhub_password2 }}
- name: Publish to certd Docker Hub repository
run: |
docker buildx imagetools create --tag certd/certd:latest greper/certd:latest
docker buildx imagetools create --tag certd/certd:${{ steps.get_certd_version.outputs.result }} greper/certd:${{ steps.get_certd_version.outputs.result }}
docker buildx imagetools create --tag certd/certd:slim greper/certd:slim
docker buildx imagetools create --tag certd/certd:${{ steps.get_certd_version.outputs.result }}-slim greper/certd:${{ steps.get_certd_version.outputs.result }}-slim
docker buildx imagetools create --tag certd/certd:armv7 greper/certd:armv7
docker buildx imagetools create --tag certd/certd:${{ steps.get_certd_version.outputs.result }}-armv7 greper/certd:${{ steps.get_certd_version.outputs.result }}-armv7
- name: deploy-certd-doc
uses: tyrrrz/action-http-request@prime
with:
+7 -11
View File
@@ -40,8 +40,8 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.dockerhub_username }}
password: ${{ secrets.dockerhub_password }}
username: ${{ secrets.dockerhub_username2 }}
password: ${{ secrets.dockerhub_password2 }}
# stable 镜像:叠加 ENV 层
- name: Build and push stable images
@@ -50,16 +50,14 @@ jobs:
ENV certd_release_mode=stable" > Dockerfile.stable
docker buildx build \
--platform linux/amd64,linux/arm64 \
--provenance=false \
--sbom=false \
--push \
-f Dockerfile.stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{ inputs.version }}-stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:${{ inputs.version }}-stable \
-t certd/certd:stable \
-t certd/certd:${{ inputs.version }}-stable \
-t ghcr.io/${{ github.repository }}:stable \
-t ghcr.io/${{ github.repository }}:${{ inputs.version }}-stable \
.
# slim-stable 镜像
@@ -69,20 +67,18 @@ jobs:
ENV certd_release_mode=stable" > Dockerfile.slim-stable
docker buildx build \
--platform linux/amd64,linux/arm64 \
--provenance=false \
--sbom=false \
--push \
-f Dockerfile.slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{ inputs.version }}-slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:${{ inputs.version }}-slim-stable \
-t certd/certd:slim-stable \
-t certd/certd:${{ inputs.version }}-slim-stable \
-t ghcr.io/${{ github.repository }}:slim-stable \
-t ghcr.io/${{ github.repository }}:${{ inputs.version }}-slim-stable \
.
- name: Set AtomGit release as stable
run: |
export ATOMGIT_TOKEN=${{ secrets.ATOMGIT_TOKEN }}
export VERSION=${{ inputs.version }}
npm run set-release-stable
node ./scripts/set-release-stable-fetch.js
+2 -1
View File
@@ -75,11 +75,12 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 只有真正机械化的大批量替换、格式化或生成任务才考虑脚本/工具。若必须使用临时脚本,应放在临时目录并在同一个受控步骤内完成创建、执行、删除;不要把临时脚本落在仓库里跨多步工具调用执行。
- 不要主动运行 `pnpm install`;缺依赖、TTY、网络导致安装或测试失败时,停止尝试并说明环境问题。
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
- 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
- 【重要重要重要】代码可读性优先于短写法。尽量少用三元表达式,优先使用完整的 `if/else`【其他技能与此条要求冲突的,以此条要求为准】;复杂条件、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
- 方法调用链不要直接塞进另一个方法参数;先用有意义的局部变量承接返回值,再传入下一步。
- 不要在单一表达式内嵌套分支、对象构造与方法调用。优先使用清晰的 `if/else` 分支;仅在确实能降低复杂度时才提取有意义的中间变量,避免为拆分而增加阅读跳转。
- 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。
- 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。
- 如无明确复用价值,不要随意新增 `normalizeXxx` 等辅助方法;优先在现有业务方法中直接处理简单兼容或字段转换逻辑,避免增加不必要的抽象层。
## 测试与验证
+45
View File
@@ -3,6 +3,51 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Bug Fixes
* 修复添加步骤会修改所有步骤标题的bug ([51e83e4](https://github.com/certd/certd/commit/51e83e4c38f7f319314fdc38ef33cf5720b2dde0))
* 修复证书插件配置无法打开的bug ([5563abb](https://github.com/certd/certd/commit/5563abb13a62140f4dee668ab768cf4ff9371ddb))
### Performance Improvements
* 新增余额管理,管理员充值功能 ([bded368](https://github.com/certd/certd/commit/bded368cbb8974b6937ae260e448c25fae5c33ad))
* **acme-account-access:** AcmeAccount授权允许修改eab,重新生成account ([6a649aa](https://github.com/certd/certd/commit/6a649aaca083ccd3b517f7d676a58220aa33b37d))
* **ui:** 商业版套餐购买不要求设置支付方式 ([e971145](https://github.com/certd/certd/commit/e9711456860d7db40914a0c295534f7bca56f83c))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
### Bug Fixes
* **plugin-service:** 修复同步自己开发的云端插件不显示已安装状态问题 ([dac94b6](https://github.com/certd/certd/commit/dac94b64f0ceb7176e2cf07ce220be0aa5ed98d2))
* **plugin:** 修复插件配置保存与加载的相关问题 ([bd94334](https://github.com/certd/certd/commit/bd9433478a43d5d64ce6370386d0fe0aaff07d2e))
### Performance Improvements
* **plugin:** add Fastly custom TLS certificates integration ([#797](https://github.com/certd/certd/issues/797)) [@szhu25](https://github.com/szhu25) ([2a2e963](https://github.com/certd/certd/commit/2a2e963914c4dfc192cbe0a4bbb9c115e626da01))
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
### Bug Fixes
* 修复插件市场插件安装无效的bug ([15d2bd8](https://github.com/certd/certd/commit/15d2bd8484a9ddd142bf4ef03d839c74405f9e6d))
* preserve Route53 TXT record values ([#791](https://github.com/certd/certd/issues/791)) ([f5430a8](https://github.com/certd/certd/commit/f5430a8925cf6cf4e16c75f2358fbd78437e6705))
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
### Bug Fixes
* 修复编辑证书申请任务导致清空ACME账号的bug ([d67379f](https://github.com/certd/certd/commit/d67379f29725bbd6dcdefe97d101023c7fd11f03))
* 修复大量问题 ([f911e06](https://github.com/certd/certd/commit/f911e06a698d568ae228880ec89b7e25fdd1305b))
* 修复旧流水线保存时清空证书仓库证书内容的bug ([f58f2f3](https://github.com/certd/certd/commit/f58f2f3cd603c37e55ac5868fdd2ca99aa937e44))
* **pipeline-controller:** 处理非编辑场景下保留id的问题 ([6606f32](https://github.com/certd/certd/commit/6606f3267868e3aadaca0092ef4c83198312553c))
* **pipeline:** 修正流水线保存时的ID判断逻辑并增加参数校验 ([6af12d4](https://github.com/certd/certd/commit/6af12d49fce1144943dc37ba574b3c9356998dcd))
### Performance Improvements
* **dashboard:** 新增仪表盘站点监控和域名管理的统计卡片 ([9842a24](https://github.com/certd/certd/commit/9842a24a50a841f710ee7c8050a38e3334c58394))
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Bug Fixes
+19 -23
View File
@@ -105,13 +105,17 @@ https://certd.handfree.work/
#### Docker镜像说明:
##### 1. 镜像地址格式:
##### 1. 镜像地址:
```
registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型
```
##### 2. 版本标签:
| 镜像仓库 | 最新预览版 | 稳定版 | slim版 | armv7版 |
| --- | --- | --- | --- | --- |
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
| Docker Hub | `certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
> 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
##### 2. 版本标签说明:
**最新版本标签:**
@@ -124,25 +128,17 @@ registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
> 根据基础镜像不同,分为如下三个分支版本,没有特殊需求选择默认的即可(他们功能是一样的)
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 |
| --- | --- | --- | --- | --- |
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小 | `certd:stable` |
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` |
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` |
##### 2. 镜像地址:
| 镜像仓库 | 最新预览版 | slim | armv7 |
| --- | --- | --- | --- |
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` |
| Docker Hub | `greper/certd:latest` | `certd:slim` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` |
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 | 指定版本 |
| --- | --- | --- | --- | --- | --- |
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小 | `certd:stable` | `certd:1.43.0` |
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` | `certd:1.43.0-slim` |
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` | `certd:1.43.0-armv7` |
> 注意:
> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
> 2. 稳定版在后面加 `-stable` 即可。
> 3. 如需指定具体的版本号,在冒号后面加 `version-`即可,例如 `certd:1.42.1-stable`。
> 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
最新版本号:
![GitHub release (latest by date)](https://img.shields.io/github/v/release/certd/certd?label=Latest%20Release&sort=semver)
##### 3. 镜像构建说明:
+9 -3
View File
@@ -2,9 +2,9 @@ version: '3.3' # 兼容旧版docker-compose
services:
certd:
# 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如:certd:1.29.0
image: registry.cn-shenzhen.aliyuncs.com/certd/certd:latest
image: registry.cn-shenzhen.aliyuncs.com/certd/certd:latest # 稳定版为 certd/certd:stable
# image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源
# image: greper/certd:latest
# image: certd/certd:latest
# --------- 生产建议使用稳定版, latest改成stable即可
# image: registry.cn-shenzhen.aliyuncs.com/certd/certd:stable
@@ -77,7 +77,13 @@ services:
# - certd_typeorm_dataSource_default_password=yourpasswd # 密码
# - certd_typeorm_dataSource_default_database=certd # 数据库名
# ↓↓↓↓ --------------------------------------------------------- 自动升级,上面certd的版本号要保持为latest
# #↓↓↓↓ ---------------------------- 如果数据库服务端开启了加密传输 require_secure_transport-ON
# - certd_typeorm_dataSource_default_ssl_rejectUnauthorized=false # 忽略证书校验,或者配置下面的三项路径
# - certd_typeorm_dataSource_default_ssl_ca # 证书CA路径【如果上面false,无需配置】
# - certd_typeorm_dataSource_default_ssl_key # 私钥路径【如果上面false,无需配置】
# - certd_typeorm_dataSource_default_ssl_cert # 证书路径【如果上面false,无需配置】
# ↓↓↓↓ --------------------------------------------------------- 自动升级,上面certd的版本号要保持为latest / stable
# certd-updater: # 添加 Watchtower 服务
# image: containrrr/watchtower:latest
# container_name: certd-updater
+72
View File
@@ -3,6 +3,78 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
### Bug Fixes
* **plugin-service:** 修复同步自己开发的云端插件不显示已安装状态问题 ([dac94b6](https://github.com/certd/certd/commit/dac94b64f0ceb7176e2cf07ce220be0aa5ed98d2))
* **plugin:** 修复插件配置保存与加载的相关问题 ([bd94334](https://github.com/certd/certd/commit/bd9433478a43d5d64ce6370386d0fe0aaff07d2e))
### Performance Improvements
* **plugin:** add Fastly custom TLS certificates integration ([#797](https://github.com/certd/certd/issues/797)) [@szhu25](https://github.com/szhu25) ([2a2e963](https://github.com/certd/certd/commit/2a2e963914c4dfc192cbe0a4bbb9c115e626da01))
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
### Bug Fixes
* 修复插件市场插件安装无效的bug ([15d2bd8](https://github.com/certd/certd/commit/15d2bd8484a9ddd142bf4ef03d839c74405f9e6d))
* preserve Route53 TXT record values ([#791](https://github.com/certd/certd/issues/791)) ([f5430a8](https://github.com/certd/certd/commit/f5430a8925cf6cf4e16c75f2358fbd78437e6705))
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
### Bug Fixes
* 修复编辑证书申请任务导致清空ACME账号的bug ([d67379f](https://github.com/certd/certd/commit/d67379f29725bbd6dcdefe97d101023c7fd11f03))
* 修复大量问题 ([f911e06](https://github.com/certd/certd/commit/f911e06a698d568ae228880ec89b7e25fdd1305b))
* 修复旧流水线保存时清空证书仓库证书内容的bug ([f58f2f3](https://github.com/certd/certd/commit/f58f2f3cd603c37e55ac5868fdd2ca99aa937e44))
* **pipeline-controller:** 处理非编辑场景下保留id的问题 ([6606f32](https://github.com/certd/certd/commit/6606f3267868e3aadaca0092ef4c83198312553c))
* **pipeline:** 修正流水线保存时的ID判断逻辑并增加参数校验 ([6af12d4](https://github.com/certd/certd/commit/6af12d49fce1144943dc37ba574b3c9356998dcd))
### Performance Improvements
* **dashboard:** 新增仪表盘站点监控和域名管理的统计卡片 ([9842a24](https://github.com/certd/certd/commit/9842a24a50a841f710ee7c8050a38e3334c58394))
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Bug Fixes
* 修复证书申请时EAB判断逻辑,已选择Acme账号的情况下不需要EAB ([d87821e](https://github.com/certd/certd/commit/d87821ee02b17136fc925df4547223332bf22823))
* **pipeline:** 校验定时触发器cron表达式,拦截2月31号等无效日期 ([#785](https://github.com/certd/certd/issues/785)) [@yukaidi1220](https://github.com/yukaidi1220) ([f8e492d](https://github.com/certd/certd/commit/f8e492d6cb49257e56bbd06b28fd6eb32c923c28))
* **plugin-ai-dev:** 优化AI插件测试流程并修复日志获取问题 ([faf9828](https://github.com/certd/certd/commit/faf9828168f6ce82adc530a65eaf313d6c9904b5))
* **plugin-item-card:** 移除分数格式化时去掉.0的逻辑 ([a91d7c0](https://github.com/certd/certd/commit/a91d7c000a2aa6456a249433ff90a47787ec90f0))
* **ssh:** 修复 keyboard-interactive 认证可能抛类型错误的问题 ([5e867c8](https://github.com/certd/certd/commit/5e867c86c30eb43f1e5aeee8c22d47596e173a8c))
* **start.sh:** 优化前端zip包下载校验逻辑 ([15ee6ea](https://github.com/certd/certd/commit/15ee6ead49db3620d2ade641aadbfcb13ac3e6dc))
* **tencent access:** 修复腾讯云国际站接口endpoint拼接问题 ([f614872](https://github.com/certd/certd/commit/f61487284f79d3f5a72c9f7eee04999a93728326))
### Features
* 实现自定义ACME证书颁发机构支持 ([97c417b](https://github.com/certd/certd/commit/97c417b1ad64b9a14bee8bfe98a2e789b9278820))
* 新增证书吊销后置任务与证书状态管理 ([1cc9641](https://github.com/certd/certd/commit/1cc9641f5669710d1e5809995ad395c843855a12))
* 新增AI插件开发全流程能力 ([6f9dffe](https://github.com/certd/certd/commit/6f9dffe6e59cc125bd394d7db6086d66a9c71e49))
* 支持审计日志,操作日志 ([f2855d6](https://github.com/certd/certd/commit/f2855d6dac74c9db71d92f99601e35c360b10666))
* 支持在线插件下载安装审核等 ([255960d](https://github.com/certd/certd/commit/255960d434eebb3d567a08f737f1642ab417b5d0))
### Performance Improvements
* 统一镜像地址格式 certd/certd ([49007d3](https://github.com/certd/certd/commit/49007d391565099d94dec375eb88e946856506b0))
* 新增AI插件开发受限令牌与隔离API ([3a9069f](https://github.com/certd/certd/commit/3a9069f6e6242940229fd63c686f7e445408dbbb))
* 优化源码启动脚本,增加快速启动 ([0cfc3b7](https://github.com/certd/certd/commit/0cfc3b7ae90fa031d75f0b395556a04d4efee76c))
* 统一将路由前缀从/certd改为/cert ([4db0f83](https://github.com/certd/certd/commit/4db0f83304dc6fd9199673c48b1805421f024601))
* 新增官方客户端Certd Client的多语言配置与页面入口 ([fbfe125](https://github.com/certd/certd/commit/fbfe1256a5c8be6dfb2fbae7e6130eb5f6c6d946))
* 新增证书吊销功能,添加证书状态字段 ([5074572](https://github.com/certd/certd/commit/5074572694383c8e1bf2880e26827987683dc62c))
* 新增自定义ACME管理功能,完善证书颁发机构适配 ([2ab23f5](https://github.com/certd/certd/commit/2ab23f50b216e27495fc823d2269ca0ba6366d15))
* 移除全局反向代理配置,迁移到自定义ACME配置 ([c57110e](https://github.com/certd/certd/commit/c57110e42aada31dd0028b56b5baec1b19722525))
* 在线插件支持通知类型 ([c0f194f](https://github.com/certd/certd/commit/c0f194f4438bd7ca3f52f63ca088543abde7428c))
* 增加稳定版,预览版区分,预览版是最新版,稳定版在预览版验证稳定后,再将预览版转为稳定版 ([1574090](https://github.com/certd/certd/commit/15740904e5e97ee767c39e4401dbc1d645795c38))
* 支持AI开发在线插件 ([eae7edb](https://github.com/certd/certd/commit/eae7edbda0fd7357163f33fb4084fcba21396f5f))
* **domain:** 域名管理增加remark字段 ([e4041ea](https://github.com/certd/certd/commit/e4041eabece1bab2c1c4b955129cf7a7fe7302d0))
* **login,settings:** 仅第三方登录开启后,在重启10分钟后将无法使用密码登录 ([1d12ad7](https://github.com/certd/certd/commit/1d12ad73c6ca827d2eb36bda867f9d9fe62dab48))
* **plugin:** 插件安装支持检测依赖 ([e79660c](https://github.com/certd/certd/commit/e79660c5d17ed4049508a00a359950a54470332f))
* **plugin:** 新增插件评分展示、排序与个人插件筛选功能 ([7ab5029](https://github.com/certd/certd/commit/7ab502991e0c001777b268b9f159311ef5e000a0))
* **server:** response优化为gzip压缩 ([f38522c](https://github.com/certd/certd/commit/f38522ca0b2185f7f35538f53cb51dcf86d0bfc8))
* **tencent-clb:** 重构腾讯云CLB部署插件的地域选择组件,改为远程下拉选择器 ([30d4fc8](https://github.com/certd/certd/commit/30d4fc84a9e940d1d07e1f25856eb6fa13da9ba1))
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
### Bug Fixes
+2 -2
View File
@@ -125,9 +125,9 @@ docker-compose up -d
## 三、问题
### 1. 迁移到PG之后,所有表的Sequence会从1开始
### 1. 迁移到PostgreSQL之后,所有表的Sequence会从1开始
执行如下sql 语句,批量将所有表的id自增设置到当前数据的最大值
执行如下sql 语句,批量将所有表的id自增设置到当前数据的最大值【仅迁移到postgresql需要执行】
```sql
DO $$
DECLARE
+28 -9
View File
@@ -6,23 +6,42 @@
Certd 提供多种 Docker 镜像版本,您可以根据需要选择:
**最新版本:**
#### 1. 镜像地址:
| 镜像仓库 | 最新预览版 | 稳定版 | slim版 | armv7版 |
| --- | --- | --- | --- | --- |
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
| Docker Hub | `certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
> 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
#### 2. 版本标签说明:
**最新版本标签:**
| 版本 | 标签 | 说明 |
| --- | --- | --- |
| 预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 |
| 稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 |
| 最新预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 |
| 最新稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 |
**系统版本分支:**
**系统分支版本:**
> 根据基础镜像不同,分为如下三个分支版本,没有特殊需求选择默认的即可(他们功能是一样的)
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 | 指定版本 |
| --- | --- | --- | --- | --- | --- |
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小 | `certd:stable` | `certd:1.43.0` |
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` | `certd:1.43.0-slim` |
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` | `certd:1.43.0-armv7` |
| 分支版本标签 | 基础系统 | 说明 | 指定版本 | 稳定版 |
| --- | --- | --- | --- | --- |
| `certd:latest` 【默认】 | Alpine Linux | 默认版本,镜像体积小 | `certd:[version]` | `certd:[version-]stable` |
| `certd:slim` | Debian slim | glibc版本,dns解析兼容性更好(可能需要配置security_opt -seccomp=unconfined)| `certd:[version-]slim` | `certd:[version-]slim-stable` |
| `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:[version]-armv7` | `certd:[version-]armv7-ststable` |
> 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
最新版本号:
![GitHub release (latest by date)](https://img.shields.io/github/v/release/certd/certd?label=Latest%20Release&sort=semver)
### 一键脚本安装(推荐)
如果您的服务器未安装 Docker,该脚本会自动为您安装 Docker 和 Docker Compose,并启动 Certd 容器。
+47 -46
View File
@@ -36,52 +36,53 @@
| 32.| **Dokploy授权** | |
| 33.| **Dynadot授权** | |
| 34.| **farcdn授权** | |
| 35.| **FlexCDN授权** | |
| 36.| **Gcore** | Gcore |
| 37.| **Github授权** | |
| 38.| **godaddy授权** | |
| 39.| **HiPM DNSMgr** | HiPM DNSMgr API Token 授权 |
| 40.| **金山云授权** | |
| 41.| **FTP授权** | |
| 42.| **七牛OSS授权** | |
| 43.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 |
| 44.| **s3/minio授权** | S3/minio oss授权 |
| 45.| **namesilo授权** | |
| 46.| **Next Terminal 授权** | 用于访问 Next Terminal API 的授权配置 |
| 47.| **Nginx Proxy Manager 授权** | 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 |
| 48.| **1panel授权** | 账号和密码 |
| 49.| **支付宝** | |
| 50.| **白山云授权** | |
| 51.| **宝塔云WAF授权** | 用于连接和管理宝塔云WAF服务的授权配置 |
| 52.| **cdnfly授权** | |
| 53.| **k8s授权** | |
| 54.| **括彩云cdn授权** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
| 55.| **LeCDN授权** | |
| 56.| **lucky** | |
| 57.| **猫云授权** | |
| 58.| **plesk授权** | |
| 59.| **长亭雷池授权** | |
| 60.| **群晖登录授权** | |
| 61.| **uniCloud** | unicloud授权 |
| 62.| **微信支付** | |
| 63.| **易盾rcdn授权** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
| 64.| **易发云短信** | sms.yfyidc.cn/ |
| 65.| **易盾DCDN授权** | https://user.yiduncdn.com |
| 66.| **易支付** | |
| 67.| **proxmox** | |
| 68.| **Spaceship.com 授权** | Spaceship.com API 授权插件 |
| 69.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器授权 |
| 70.| **UCloud授权** | 优刻得授权 |
| 71.| **又拍云** | |
| 72.| **网宿授权** | |
| 73.| **西部数码授权** | |
| 74.| **我爱云授权** | 我爱云CDN |
| 75.| **新网授权(代理方式)** | |
| 76.| **新网授权** | |
| 77.| **新网互联授权** | 仅支持代理账号,ip需要加入白名单 |
| 78.| **Zenlayer授权** | Zenlayer授权 |
| 79.| **GoEdge授权** | |
| 80.| **雨云授权** | https://app.rainyun.com/ |
| 35.| **Fastly授权** | Fastly CDN / TLS Custom Certificates API 授权 |
| 36.| **FlexCDN授权** | |
| 37.| **Gcore** | Gcore |
| 38.| **Github授权** | |
| 39.| **godaddy授权** | |
| 40.| **HiPM DNSMgr** | HiPM DNSMgr API Token 授权 |
| 41.| **金山云授权** | |
| 42.| **FTP授权** | |
| 43.| **七牛OSS授权** | |
| 44.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 |
| 45.| **s3/minio授权** | S3/minio oss授权 |
| 46.| **namesilo授权** | |
| 47.| **Next Terminal 授权** | 用于访问 Next Terminal API 的授权配置 |
| 48.| **Nginx Proxy Manager 授权** | 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 |
| 49.| **1panel授权** | 账号和密码 |
| 50.| **支付宝** | |
| 51.| **白山云授权** | |
| 52.| **宝塔云WAF授权** | 用于连接和管理宝塔云WAF服务的授权配置 |
| 53.| **cdnfly授权** | |
| 54.| **k8s授权** | |
| 55.| **括彩云cdn授权** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
| 56.| **LeCDN授权** | |
| 57.| **lucky** | |
| 58.| **猫云授权** | |
| 59.| **plesk授权** | |
| 60.| **长亭雷池授权** | |
| 61.| **群晖登录授权** | |
| 62.| **uniCloud** | unicloud授权 |
| 63.| **微信支付** | |
| 64.| **易盾rcdn授权** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
| 65.| **易发云短信** | sms.yfyidc.cn/ |
| 66.| **易盾DCDN授权** | https://user.yiduncdn.com |
| 67.| **易支付** | |
| 68.| **proxmox** | |
| 69.| **Spaceship.com 授权** | Spaceship.com API 授权插件 |
| 70.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器授权 |
| 71.| **UCloud授权** | 优刻得授权 |
| 72.| **又拍云** | |
| 73.| **网宿授权** | |
| 74.| **西部数码授权** | |
| 75.| **我爱云授权** | 我爱云CDN |
| 76.| **新网授权(代理方式)** | |
| 77.| **新网授权** | |
| 78.| **新网互联授权** | 仅支持代理账号,ip需要加入白名单 |
| 79.| **Zenlayer授权** | Zenlayer授权 |
| 80.| **GoEdge授权** | |
| 81.| **雨云授权** | https://app.rainyun.com/ |
<style module>
table th:first-of-type {
+25 -21
View File
@@ -1,5 +1,5 @@
# 任务插件
共 `135` 款任务插件
共 `139` 款任务插件
## 1. 证书申请
| 序号 | 名称 | 说明 |
@@ -31,26 +31,30 @@
| 4.| **中国移动-部署证书到CDN** | 中国移动自动部署证书到CDN |
| 5.| **多吉云-部署到多吉云CDN** | |
| 6.| **farcdn-更新证书** | www.farcdn.net |
| 7.| **FlexCDN-更新证书** | |
| 8.| **Gcore-刷新Gcore证书** | 刷新现有的证书 |
| 9.| **Gcore-部署证书到Gcore** | 仅上传 并不会部署到cdn |
| 10.| **GoEdge-更新证书** | GoEdge |
| 11.| **金山云-更新CDN证书** | 金山云自动更新CDN证书 |
| 12.| **白山云-更新证书** | |
| 13.| **cdnfly-部署证书到cdnfly** | cdnfly |
| 14.| **天翼云-部署证书到CDN** | 部署证书到天翼云CDN和全站加速 |
| 15.| **括彩云-部署到括彩云CDN** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
| 16.| **LeCDN-更新证书V2** | 支持新版本LeCDN |
| 17.| **LeCDN-更新证书** | |
| 18.| **Maoyun-更新猫云CDN证书** | |
| 19.| **易盾-部署到易盾DCDN** | 主要是防御,http://user.yiduncdn.com/ |
| 20.| **易盾-部署到易盾RCDN** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
| 21.| **雨云-更新证书** | app.rainyun.com |
| 22.| **又拍云-部署证书到CDN/USS** | 支持又拍云CDN,又拍云云存储USS |
| 23.| **网宿-更新证书** | 网宿证书自动更新 |
| 24.| **西数-部署到虚拟主机** | 西部数码部署证书到虚拟主机 |
| 25.| **我爱云-部署证书到我爱云CDN** | 部署证书到我爱云CDN |
| 26.| **Zenlayer-刷新证书** | 刷新Zenlayer CDN证书 |
| 7.| **Fastly-部署TLS激活** | 部署 Fastly 证书 (创建 TLS Activation 绑定证书到域名) |
| 8.| **Fastly-清除缓存** | 清除 Fastly 指定 Service 的所有缓存 (Purge All) |
| 9.| **Fastly-更新证书** | 自动更新 Fastly CDN 上的已有自定义证书 |
| 10.| **Fastly-上传证书到Fastly** | 部署 / 上传 SSL/TLS 自定义证书到 Fastly CDN |
| 11.| **FlexCDN-更新证书** | |
| 12.| **Gcore-刷新Gcore证书** | 刷新现有的证书 |
| 13.| **Gcore-部署证书到Gcore** | 仅上传 并不会部署到cdn |
| 14.| **GoEdge-更新证书** | GoEdge |
| 15.| **金山云-更新CDN证书** | 金山云自动更新CDN证书 |
| 16.| **白山云-更新证书** | |
| 17.| **cdnfly-部署证书到cdnfly** | cdnfly |
| 18.| **天翼云-部署证书到CDN** | 部署证书到天翼云CDN和全站加速 |
| 19.| **括彩云-部署到括彩云CDN** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
| 20.| **LeCDN-更新证书V2** | 支持新版本LeCDN |
| 21.| **LeCDN-更新证书** | |
| 22.| **Maoyun-更新猫云CDN证书** | |
| 23.| **易盾-部署到易盾DCDN** | 主要是防御,http://user.yiduncdn.com/ |
| 24.| **易盾-部署到易盾RCDN** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
| 25.| **雨云-更新证书** | app.rainyun.com |
| 26.| **又拍云-部署证书到CDN/USS** | 支持又拍云CDN,又拍云云存储USS |
| 27.| **网宿-更新证书** | 网宿证书自动更新 |
| 28.| **西数-部署到虚拟主机** | 西部数码部署证书到虚拟主机 |
| 29.| **我爱云-部署证书到我爱云CDN** | 部署证书到我爱云CDN |
| 30.| **Zenlayer-刷新证书** | 刷新Zenlayer CDN证书 |
## 4. 面板
| 序号 | 名称 | 说明 |
+1 -1
View File
@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.44.0"
"version": "1.44.4"
}
-1
View File
@@ -53,7 +53,6 @@
"dependencies": {
"copyfiles": "^2.4.1",
"lodash-es": "^4.17.21",
"root": "link:",
"typescript": "^5.4.2"
},
"workspaces": [
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/publishlab/node-acme-client/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/acme-client
## [1.44.3](https://github.com/publishlab/node-acme-client/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/acme-client
## [1.44.2](https://github.com/publishlab/node-acme-client/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/acme-client
## [1.44.1](https://github.com/publishlab/node-acme-client/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/acme-client
# [1.44.0](https://github.com/publishlab/node-acme-client/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+4 -4
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.44.0",
"version": "1.44.4",
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.44.0",
"@certd/basic": "^1.44.4",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -58,7 +58,7 @@
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "rimraf dist && tsc --skipLibCheck --watch",
"compile": " tsc --skipLibCheck --watch",
"format": "prettier --write src"
},
"repository": {
@@ -76,5 +76,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
}
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/basic
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/basic
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/basic
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/basic
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+1 -1
View File
@@ -1 +1 @@
00:39
19:17
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.44.0",
"version": "1.44.4",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -15,7 +15,7 @@
"test": "mocha --loader=ts-node/esm",
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "rimraf dist && tsc --skipLibCheck --watch",
"compile": " tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
@@ -54,5 +54,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
}
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/pipeline
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/pipeline
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/pipeline
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/pipeline
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.44.0",
"version": "1.44.4",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,13 +16,13 @@
"test": "mocha --loader=ts-node/esm",
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "rimraf dist && tsc --skipLibCheck --watch",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.44.0",
"@certd/plus-core": "^1.44.0",
"@certd/basic": "^1.44.4",
"@certd/plus-core": "^1.44.4",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.2.2"
@@ -51,5 +51,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
}
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/lib-huawei
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/lib-huawei
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/lib-huawei
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/lib-huawei
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.44.0",
"version": "1.44.4",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -13,7 +13,7 @@
"preview": "vite preview",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish",
"compile": "rimraf dist && npm run build",
"compile": " npm run build",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
@@ -31,5 +31,5 @@
"prettier": "3.3.3",
"tslib": "^2.8.1"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
}
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/lib-iframe
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/lib-iframe
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/lib-iframe
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/lib-iframe
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.44.0",
"version": "1.44.4",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,7 +16,7 @@
"preview": "vite preview",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish",
"compile": "rimraf dist && npm run build",
"compile": " npm run build",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
@@ -37,5 +37,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
}
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/jdcloud
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/jdcloud
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/jdcloud
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/jdcloud
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
**Note:** Version bump only for package @certd/jdcloud
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.44.0",
"version": "1.44.4",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -63,5 +63,5 @@
"fetch"
]
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
}
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/lib-k8s
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/lib-k8s
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/lib-k8s
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/lib-k8s
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.44.0",
"version": "1.44.4",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,12 +16,12 @@
"preview": "vite preview",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish",
"compile": "rimraf dist && tsc --skipLibCheck --watch",
"compile": " tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.44.0",
"@certd/basic": "^1.44.4",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
}
+18
View File
@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Bug Fixes
* 修复证书插件配置无法打开的bug ([5563abb](https://github.com/certd/certd/commit/5563abb13a62140f4dee668ab768cf4ff9371ddb))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/lib-server
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/lib-server
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/lib-server
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.44.0",
"version": "1.44.4",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -17,7 +17,7 @@
"cov": "midway-bin cov --ts",
"prepublish": "npm run build",
"pub": "npm publish",
"compile": "rimraf dist && tsc --skipLibCheck --watch",
"compile": " tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
@@ -29,11 +29,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.44.0",
"@certd/basic": "^1.44.0",
"@certd/pipeline": "^1.44.0",
"@certd/plugin-lib": "^1.44.0",
"@certd/plus-core": "^1.44.0",
"@certd/acme-client": "^1.44.4",
"@certd/basic": "^1.44.4",
"@certd/pipeline": "^1.44.4",
"@certd/plugin-lib": "^1.44.4",
"@certd/plus-core": "^1.44.4",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -69,5 +69,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
}
@@ -75,7 +75,10 @@ export abstract class BaseService<T> {
if (!id) {
throw new ValidateException("id不能为空");
}
const info = await this.getRepository().findOneBy({ id } as any);
const info = await this.getRepository().findOne({
//@ts-ignore
where: { id },
});
if (info && infoIgnoreProperty) {
for (const property of infoIgnoreProperty) {
delete info[property];
@@ -114,6 +117,13 @@ export abstract class BaseService<T> {
);
}
async updateById(data: any) {
if (!data.id) {
throw new ValidateException("id 不能为空");
}
await this.addOrUpdate(data);
}
/**
* 删除
* @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3
@@ -163,6 +173,7 @@ export abstract class BaseService<T> {
const now = new Date();
param.createTime = now;
param.updateTime = now;
delete param.id;
await this.addOrUpdate(param);
await this.modifyAfter(param);
return {
@@ -175,7 +186,9 @@ export abstract class BaseService<T> {
* @param param 数据
*/
async update(param: any) {
if (!param.id) throw new ValidateException("id 不能为空");
if (!param.id) {
throw new ValidateException("id 不能为空");
}
param.updateTime = new Date();
await this.addOrUpdate(param);
await this.modifyAfter(param);
@@ -133,12 +133,12 @@ export class SysPrivateSettings extends BaseSettings {
// 内置项不在此配置 Directory URL:其端点由运行时按加密算法(pkType)通过 acme.getDirectoryUrl 获取,
// 例如 ZeroSSL、SSL.com 的 RSA 与 EC 证书使用不同的端点,写死单一 URL 会导致 EC 申请走错端点。
customAcmeProviders?: CustomAcmeProvider[] = [
{ sslProvider: "letsencrypt", title: "Let's Encrypt", directoryUrl: "", needEAB: false, builtIn: true },
{ sslProvider: "letsencrypt_staging", title: "Let's Encrypt测试环境", directoryUrl: "", needEAB: false, builtIn: true },
{ sslProvider: "letsencrypt", title: "Let's Encrypt(支持7天IP证书)", directoryUrl: "", needEAB: false, builtIn: true },
{ sslProvider: "google", title: "Google", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "zerossl", title: "ZeroSSL", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "litessl", title: "litessl", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "litessl", title: "Litessl", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "sslcom", title: "SSL.com", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "letsencrypt_staging", title: "Let's Encrypt测试环境(只能生成测试证书)", directoryUrl: "", needEAB: false, builtIn: true },
];
removeSecret() {
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/midway-flyway-js
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.44.0",
"version": "1.44.4",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -17,7 +17,7 @@
"cov": "midway-bin cov --ts",
"prepublish": "npm run build",
"pub": "npm publish",
"compile": "rimraf dist && npm run build",
"compile": " npm run build",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
@@ -52,5 +52,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
}
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/plugin-cert
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/plugin-cert
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/plugin-cert
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/plugin-cert
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
**Note:** Version bump only for package @certd/plugin-cert
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.44.0",
"version": "1.44.4",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -20,7 +20,7 @@
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/plugin-lib": "^1.44.0"
"@certd/plugin-lib": "^1.44.4"
},
"devDependencies": {
"@types/chai": "^4.3.12",
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
}
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/plugin-lib
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/plugin-lib
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/plugin-lib
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/plugin-lib
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Bug Fixes
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.44.0",
"version": "1.44.4",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -12,14 +12,14 @@
"dev-build": "npm run build",
"test:unit": "cross-env NODE_ENV=unittest mocha",
"pub": "npm publish",
"compile": "rimraf dist && tsc --skipLibCheck --watch",
"compile": " tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/acme-client": "^1.44.0",
"@certd/basic": "^1.44.0",
"@certd/pipeline": "^1.44.0",
"@certd/acme-client": "^1.44.4",
"@certd/basic": "^1.44.4",
"@certd/pipeline": "^1.44.4",
"dayjs": "^1.11.7",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
@@ -45,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
}
@@ -40,6 +40,7 @@ export function createRemoteSelectInputDefine(opts?: {
rules?: any;
mergeScript?: string;
search?: boolean;
emitImmediate?: boolean;
pager?: boolean;
component?: any;
value?: any;
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Bug Fixes
* 修复添加步骤会修改所有步骤标题的bug ([51e83e4](https://github.com/certd/certd/commit/51e83e4c38f7f319314fdc38ef33cf5720b2dde0))
* 修复证书插件配置无法打开的bug ([5563abb](https://github.com/certd/certd/commit/5563abb13a62140f4dee668ab768cf4ff9371ddb))
### Performance Improvements
* 新增余额管理,管理员充值功能 ([bded368](https://github.com/certd/certd/commit/bded368cbb8974b6937ae260e448c25fae5c33ad))
* **ui:** 商业版套餐购买不要求设置支付方式 ([e971145](https://github.com/certd/certd/commit/e9711456860d7db40914a0c295534f7bca56f83c))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
### Bug Fixes
* **plugin:** 修复插件配置保存与加载的相关问题 ([bd94334](https://github.com/certd/certd/commit/bd9433478a43d5d64ce6370386d0fe0aaff07d2e))
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
### Bug Fixes
* 修复插件市场插件安装无效的bug ([15d2bd8](https://github.com/certd/certd/commit/15d2bd8484a9ddd142bf4ef03d839c74405f9e6d))
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
### Bug Fixes
* 修复编辑证书申请任务导致清空ACME账号的bug ([d67379f](https://github.com/certd/certd/commit/d67379f29725bbd6dcdefe97d101023c7fd11f03))
* 修复大量问题 ([f911e06](https://github.com/certd/certd/commit/f911e06a698d568ae228880ec89b7e25fdd1305b))
* **pipeline:** 修正流水线保存时的ID判断逻辑并增加参数校验 ([6af12d4](https://github.com/certd/certd/commit/6af12d49fce1144943dc37ba574b3c9356998dcd))
### Performance Improvements
* **dashboard:** 新增仪表盘站点监控和域名管理的统计卡片 ([9842a24](https://github.com/certd/certd/commit/9842a24a50a841f710ee7c8050a38e3334c58394))
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.44.0",
"version": "1.44.4",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -104,8 +104,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.44.0",
"@certd/pipeline": "^1.44.0",
"@certd/lib-iframe": "^1.44.4",
"@certd/pipeline": "^1.44.4",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -399,7 +399,7 @@ watch(
width: 80px;
}
.input {
width: 150px;
width: 180px;
}
}
}
@@ -71,6 +71,7 @@ const props = defineProps<
pager?: boolean;
single?: boolean;
pageSize?: number;
emitImmediate?: boolean;
uploadCert?: UploadCertProps;
} & ComponentPropsType
>();
@@ -210,8 +211,9 @@ const getOptions = async () => {
if (isFirst) {
isFirst = false;
if (props.value) {
if (props.value && props.emitImmediate) {
emitSelectedChange(props.value);
emit("update:value", props.value);
}
return;
}
@@ -14,6 +14,15 @@ export default {
certCount: "Number of Certificates",
noCert: "You have no certificates yet",
manageCert: "View Certificates",
siteMonitorCount: "Number of Monitored Sites",
siteAbnormalCount: "Abnormal",
siteNormalCount: "Normal",
manageSiteMonitor: "Manage Monitored Sites",
domainCount: "Number of Domains",
domainExpiringCount: "Expiring",
domainExpiredCount: "Expired",
domainNotExpiredCount: "Not Expired",
manageDomain: "Manage Domains",
certExpiringCount: "Soon-to-Expire",
certExpiredCount: "Expired",
certNoExpireCount: "Not Expired",
@@ -23,6 +32,16 @@ export default {
recentRun: "Recent Run Statistics",
runCount: "Run Count",
expiringCerts: "Soon-to-Expire Certificates",
totalGeneratedCertCount: "Total Certificates Generated",
singleDomainCertShortCount: "Single-domain",
multiDomainCertShortCount: "Multi-domain",
wildcardCertShortCount: "Wildcard",
singleDomainCertCount: "Single-domain Certificates Generated",
multiDomainCertCount: "Multi-domain Certificates Generated",
wildcardCertCount: "Wildcard Certificates Generated",
savedAmount: "Estimated savings",
savedAmountRule: "Estimated at one quarter of Tencent Cloud's annual certificate prices: single-domain ¥532, multi-domain ¥912, and wildcard ¥2147. Actual savings may vary.",
totalPipelineRuns: "Total Pipeline Runs",
supportedTasks: "Overview of Supported Deployment Tasks",
changeLog: "Change Log",
stableRelease: "Stable",
@@ -53,6 +53,8 @@ export default {
suiteSetting: "Suite Settings",
orderManager: "Order Management",
userSuites: "User Suites",
balanceManager: "Balance Management",
balanceLog: "Balance Change Records",
inviteCommissionSetting: "Incentive Plan Settings",
inviteLevel: "Promotion Levels",
inviteUserLevel: "User Promotion Levels",
@@ -17,6 +17,15 @@ export default {
certNoExpireCount: "未过期",
noCert: "还没有证书",
manageCert: "查看证书",
siteMonitorCount: "监控站点数量",
siteAbnormalCount: "异常",
siteNormalCount: "正常",
manageSiteMonitor: "管理监控站点",
domainCount: "域名数量",
domainExpiringCount: "即将到期",
domainExpiredCount: "已过期",
domainNotExpiredCount: "未到期",
manageDomain: "管理域名",
createNow: "立即创建",
managePipeline: "管理流水线",
@@ -24,6 +33,16 @@ export default {
recentRun: "最近运行统计",
runCount: "运行次数",
expiringCerts: "最快到期证书",
totalGeneratedCertCount: "累计生成证书数量",
singleDomainCertShortCount: "单域名",
multiDomainCertShortCount: "多域名",
wildcardCertShortCount: "泛域名",
singleDomainCertCount: "累计单域名证书数量",
multiDomainCertCount: "累计多域名证书数量",
wildcardCertCount: "累计泛域名证书数量",
savedAmount: "已为您累计节省",
savedAmountRule: "按腾讯云证书平台一年期单价的 1/4 进行估算。实际节省金额仅供参考。(腾讯云单域名证书 532 元,多域名证书 912 元,泛域名证书 2147 元)",
totalPipelineRuns: "累计流水线运行次数",
supportedTasks: "已支持的部署任务总览",
changeLog: "更新日志",
stableRelease: "稳定版",
@@ -54,6 +54,8 @@ export default {
suiteSetting: "套餐设置",
orderManager: "订单管理",
userSuites: "用户套餐",
balanceManager: "余额管理",
balanceLog: "余额变更记录",
inviteCommissionSetting: "激励计划设置",
inviteLevel: "推广等级",
inviteUserLevel: "用户推广等级",
@@ -317,6 +317,34 @@ export const sysResources = [
keepAlive: true,
},
},
{
title: "certd.sysResources.balanceManager",
name: "SysWallet",
path: "/sys/suite/wallet",
component: "/sys/suite/wallet/index.vue",
meta: {
show: () => {
const settingStore = useSettingStore();
return settingStore.isComm;
},
icon: "ion:wallet-outline",
permission: "sys:settings:edit",
auth: true,
keepAlive: true,
},
},
{
title: "certd.sysResources.balanceLog",
name: "SysWalletLog",
path: "/sys/suite/wallet-log",
component: "/sys/suite/wallet/log/index.vue",
meta: {
isMenu: false,
permission: "sys:settings:view",
auth: true,
keepAlive: false,
},
},
{
title: "certd.sysResources.inviteCommissionSetting",
name: "SysInviteCommissionSetting",
@@ -1,13 +1,62 @@
import { DynamicType, FormItemProps, useMerge } from "@fast-crud/fast-crud";
import { merge, set } from "lodash-es";
import { defineStore } from "pinia";
import * as api from "./api.plugin";
import { DynamicType, FormItemProps, useMerge } from "@fast-crud/fast-crud";
import { i18n } from "/src/locales/i18n";
import { cloneDeep } from "lodash-es";
interface PluginState {
group?: PluginGroups;
originGroup?: PluginGroups;
}
export function fillPluginDefine(plugin: any) {
const columns = plugin.input;
for (const key in columns) {
const column = columns[key];
if (!column.component) {
column.component = {
name: "a-input",
vModel: "value",
};
} else {
if (column.component.vModel == null && column.component.name?.startsWith("a-")) {
column.component.vModel = "value";
}
}
}
if (plugin.sysSetting?.metadata) {
merge(plugin.input, plugin.sysSetting.metadata?.input || {});
const keys = Object.keys(plugin.input);
for (const key of keys) {
//修复旧版本错误引入的key导致报错的问题 【多引入进了一个type=CertApply】
const column = plugin.input[key];
if (typeof column != "object") {
delete plugin.input[key];
}
}
// 应用选项映射
for (const key of Object.keys(plugin.input)) {
const inputDef = plugin.input[key];
if (inputDef.optionsMapping && inputDef.component?.options) {
const mapping = inputDef.optionsMapping;
for (const opt of inputDef.component.options) {
if (mapping[opt.value] !== undefined) {
opt.label = mapping[opt.value];
}
}
}
}
}
// if (plugin.sysSetting?.input) {
// // 设置系统默认值, 不能放这里设置
// for (const key in plugin.input) {
// const sysValue = plugin.sysSetting?.input[key];
// if (sysValue != undefined) {
// set(plugin, `input.${key}.value`, sysValue);
// }
// }
// }
}
export type PluginGroup = {
key: string;
title: string;
@@ -28,6 +77,7 @@ export type PluginDefine = {
output: {
[key: string]: any;
};
type?: string;
};
export class PluginGroups {
@@ -35,10 +85,9 @@ export class PluginGroups {
map!: { [key: string]: PluginDefine };
t: any;
mergeSetting?: boolean;
constructor(groups: { [key: string]: PluginGroup }, opts?: { mergeSetting?: boolean }) {
constructor(groups: { [key: string]: PluginGroup }) {
this.groups = groups;
this.t = i18n.global.t;
this.mergeSetting = opts?.mergeSetting ?? false;
this.initGroup(groups);
this.initMap();
}
@@ -53,26 +102,9 @@ export class PluginGroups {
icon: "material-symbols:border-all-rounded",
};
for (const key in groups) {
if (this.mergeSetting) {
for (const plugin of groups[key].plugins) {
if (plugin.sysSetting) {
merge(plugin.input, plugin.sysSetting.metadata?.input || {});
// 应用选项映射
for (const key of Object.keys(plugin.input)) {
const inputDef = plugin.input[key];
if (inputDef.optionsMapping && inputDef.component?.options) {
const mapping = inputDef.optionsMapping;
for (const opt of inputDef.component.options) {
if (mapping[opt.value] !== undefined) {
opt.label = mapping[opt.value];
}
}
}
}
}
}
for (const plugin of groups[key].plugins) {
fillPluginDefine(plugin);
}
all.plugins.push(...groups[key].plugins);
}
this.groups = {
@@ -173,16 +205,12 @@ export const usePluginStore = defineStore({
id: "app.plugin",
state: (): PluginState => ({
group: null,
originGroup: null,
}),
actions: {
async reload() {
const groups = await api.GetGroups({});
const installedGroups = filterInstalledPluginGroups(groups);
this.group = new PluginGroups(installedGroups, { mergeSetting: true });
this.originGroup = new PluginGroups(cloneDeep(installedGroups));
this.group = new PluginGroups(groups);
console.log("group", this.group);
console.log("originGroup", this.originGroup);
},
async init() {
if (!this.group) {
@@ -206,10 +234,6 @@ export const usePluginStore = defineStore({
await this.init();
return this.group.get(name);
},
async getPluginDefineFromOrigin(name: string): Promise<PluginDefine> {
await this.init();
return this.originGroup.get(name);
},
async getPluginConfig(query: any) {
return await api.GetPluginConfig(query);
},
@@ -24,7 +24,8 @@ export const useProjectStore = defineStore("app.project", () => {
}
const userStore = useUserStore();
const userId = userStore.getUserInfo?.id;
const lastProjectIdCacheKey = "currentProjectId:" + userId;
const settingStore = useSettingStore();
const lastProjectIdCacheKey = `currentProjectId:${userId}`;
const lastProjectId = LocalStorage.get(lastProjectIdCacheKey);
currentProjectId.value = lastProjectId;
@@ -45,7 +46,6 @@ export const useProjectStore = defineStore("app.project", () => {
return null;
});
const settingStore = useSettingStore();
const isEnterprise = computed(() => {
return settingStore.isEnterprise;
});
@@ -390,6 +390,9 @@ export const useSettingStore = defineStore({
this.loadProductInfo();
this.inited = true;
},
getSiteId() {
return this.installInfo?.siteId || "";
},
},
});
@@ -280,6 +280,8 @@ button.ant-btn.isPlus {
align-items: center;
img {
width: 16px;
height: 16px;
&.font-size-16 {
width: 16px;
height: 16px;
@@ -51,10 +51,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
rowHandle: {
width: 120,
fixed: "right",
show: false,
buttons: {
view: { show: false },
edit: { show: false },
remove: { show: true },
remove: { show: false },
copy: { show: false },
},
},
@@ -3,7 +3,7 @@
<template #header>
<div class="title">
DNS持久验证记录
<span class="red sub" style="color: red">当前仅 Let's Encrypt 测试环境可以申请 DNS 持久验证证书。</span>
<span class="red sub" style="color: red">当前仅 Let's Encrypt 测试环境 可以申请 DNS 持久验证证书。</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
@@ -235,7 +235,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
} else if (value === "dns-persist") {
Modal.confirm({
title: "请前往DNS持久验证记录页面添加记录",
content: "DNS持久验证需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Let's Encrypt 测试环境可以申请。",
content: "DNS持久验证需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Let's Encrypt 测试环境 可以申请。",
async onOk() {
router.push({
path: "/cert/cert/dns-persist",
@@ -152,6 +152,13 @@ export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref
});
const initialForm = req.initialForm || {};
const pluginSysConfig = await pluginStore.getPluginConfig({ name: certPlugin.name, type: "builtIn" });
if (pluginSysConfig?.sysSetting?.input) {
//设置系统默认值,必须放这里设置,保证随时应用最新的系统值
for (const key in pluginSysConfig.sysSetting?.input) {
initialForm.input[key] = pluginSysConfig.sysSetting?.input[key];
}
}
initialForm.type = certPlugin.name;
const applyTemplates = reactive<any[]>([]);
const selectedTemplateId = ref<number | null>(null);
@@ -556,12 +563,12 @@ export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref
//设置系统初始值
const initialForm: any = { input: {} };
const pluginSysConfig = await pluginStore.getPluginConfig({ name: req.pluginName, type: "builtIn" });
if (pluginSysConfig.sysSetting?.input) {
for (const key in pluginSysConfig.sysSetting?.input) {
initialForm.input[key] = pluginSysConfig.sysSetting?.input[key];
}
}
// const pluginSysConfig = await pluginStore.getPluginConfig({ name: req.pluginName, type: "builtIn" });
// if (pluginSysConfig.sysSetting?.input) {
// for (const key in pluginSysConfig.sysSetting?.input) {
// initialForm.input[key] = pluginSysConfig.sysSetting?.input[key];
// }
// }
async function doSubmit({ form }: any) {
// const certDetail = readCertDetail(form.cert.crt);
// 添加certd pipeline
@@ -263,8 +263,8 @@ function useStepForm() {
currentStep.input[key] = column.default ?? column.value;
}
}
//设置系统初始值
const pluginSysConfig = await pluginStore.getPluginConfig({ name: pluginDefine.name, type: "builtIn" });
//设置系统初始值,必须放在这里设置,保证最新的系统值被清空后,这里也能跟着清空
const pluginSysConfig = await pluginStore.getPluginConfig({ name: pluginDefine.name, type: pluginDefine.type });
if (pluginSysConfig.sysSetting?.input) {
for (const key in pluginSysConfig.sysSetting?.input) {
currentStep.input[key] = pluginSysConfig.sysSetting?.input[key];
@@ -280,7 +280,10 @@ function useStepForm() {
return;
}
callback.value("save", currentStep);
// currentStep 是复用同一份 reactive 对象,直接传给回调会被 task.steps 保存同一引用,
// 后续再次 stepOpen 会 delete 掉其属性,导致已保存的步骤(如第一个步骤)被覆盖。
// 这里深拷贝一份再传出,保证每个步骤是独立对象。
callback.value("save", cloneDeep(currentStep));
stepDrawerClose();
};
@@ -53,9 +53,14 @@
<div class="flex-o mt-5">
<span class="label">{{ $t("certd.order.paymentMethod") }}:</span>
<div v-if="durationSelected.price === 0 || expectedThirdPartyAmount === 0">{{ $t("certd.order.free") }}</div>
<div v-if="durationSelected.price === 0 || expectedThirdPartyAmount === 0">0元</div>
<div v-else-if="noPaymentMethod">当前仅支持余额支付</div>
<fs-dict-select v-else v-model:value="formRef.payType" :dict="paymentsDictRef" style="width: 200px"> </fs-dict-select>
</div>
<div v-if="showBalanceInsufficientTip" class="flex-o mt-5">
<span class="color-red">余额不足,请联系管理员,充值或设置套餐</span>
</div>
</div>
</a-modal>
</template>
@@ -88,12 +93,17 @@ async function open(opts: OrderModalOpenReq) {
formRef.value.productId = opts.product.id;
formRef.value.duration = opts.duration;
formRef.value.num = opts.num ?? 1;
formRef.value.useRebateBalance = false;
formRef.value.useRebateBalance = true;
try {
wallet.value = await GetWalletSummary();
} catch (e) {
wallet.value = { availableAmount: 0 };
}
try {
await paymentsDictRef.loadDict();
} catch (e) {
// 支付方式加载失败时按无可用支付方式处理
}
}
const paymentsDictRef = dict({
async getData() {
@@ -119,6 +129,23 @@ const expectedThirdPartyAmount = computed(() => {
return Math.max(0, (durationSelected.value?.price || 0) - expectedRebateAmount.value);
});
const noPaymentMethod = computed(() => {
return paymentsDictRef.data.length === 0;
});
const balanceInsufficient = computed(() => {
const price = durationSelected.value?.price || 0;
if (price <= 0) {
return false;
}
const availableAmount = wallet.value?.availableAmount || 0;
return availableAmount < price;
});
const showBalanceInsufficientTip = computed(() => {
return noPaymentMethod.value && balanceInsufficient.value;
});
function amountToYuan(amount: number) {
return util.amount.toYuan(amount || 0);
}
@@ -8,7 +8,8 @@ import { CanvasRenderer } from "echarts/renderers";
import { PieChart } from "echarts/charts";
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from "echarts/components";
import VChart, { THEME_KEY } from "vue-echarts";
import { ref, provide, defineProps } from "vue";
import { computed, provide, defineProps } from "vue";
import { usePreferences } from "/@/vben/preferences";
import { ChartItem } from "./d";
use([CanvasRenderer, PieChart, TitleComponent, TooltipComponent, GridComponent, LegendComponent]);
@@ -18,16 +19,26 @@ provide(THEME_KEY, "");
const props = defineProps<{
data: ChartItem[];
}>();
const { isDark } = usePreferences();
const option = ref({
const option = computed(() => ({
color: ["#91cc75", "#73c0de", "#ee6666", "#fac858", "#5470c6", "#3ba272", "#fc8452", "#9a60b4", "#ea7ccc", "#5470c6"],
tooltip: {
trigger: "item",
backgroundColor: isDark.value ? "rgba(35, 38, 45, 0.96)" : "rgba(255, 255, 255, 0.96)",
borderColor: isDark.value ? "rgba(255, 255, 255, 0.2)" : "#d9d9d9",
textStyle: {
color: isDark.value ? "rgba(242, 242, 242, 0.92)" : "#333",
},
},
legend: {
orient: "vertical",
bottom: "5%",
left: "left",
textStyle: {
color: isDark.value ? "rgba(242, 242, 242, 0.85)" : "#555",
},
inactiveColor: isDark.value ? "rgba(242, 242, 242, 0.35)" : "#ccc",
},
grid: {
top: "20px",
@@ -65,7 +76,7 @@ const option = ref({
data: props.data,
},
],
});
}));
</script>
<style lang="less">
@@ -65,7 +65,7 @@
<SimpleSteps></SimpleSteps>
</div>
</div>
<div class="warning">
<div v-if="noticeList.length > 0" class="warning">
<notice-bar :list="noticeList"></notice-bar>
</div>
@@ -84,11 +84,22 @@
</template>
</statistic-card>
</a-col>
<!-- <a-col :md="6" :xs="24">
<a-col :md="6" :xs="24">
<statistic-card :title="t('certd.dashboard.pipelineStatus')" :footer="false">
<pie-count v-if="count.pipelineStatusCount" :data="count.pipelineStatusCount"></pie-count>
<pie-count v-if="count.pipelineStatusCount" :data="count.pipelineStatusCount" />
</statistic-card>
</a-col> -->
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:data-trending-24" :title="t('certd.dashboard.recentRun')" :footer="false">
<day-count v-if="count.historyCountPerDay" :data="count.historyCountPerDay" :title="t('certd.dashboard.runCount')"></day-count>
</statistic-card>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:alert-urgent-24" :title="t('certd.dashboard.expiringCerts')">
<expiring-list v-if="count.expiringList" :data="count.expiringList"></expiring-list>
</statistic-card>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:certificate-24" :title="t('certd.dashboard.certCount')" :count="count.certCount" link="/cert/monitor/cert" :sub-counts="count.certStatusCount">
<template v-if="count.certCount === 0" #default>
@@ -102,13 +113,29 @@
</statistic-card>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:data-trending-24" :title="t('certd.dashboard.recentRun')" :footer="false">
<day-count v-if="count.historyCountPerDay" :data="count.historyCountPerDay" :title="t('certd.dashboard.runCount')"></day-count>
<statistic-card
icon="fluent-color:certificate-24"
:title="t('certd.dashboard.totalGeneratedCertCount')"
:count="count.totalGeneratedCertCount"
:sub-counts="count.generatedCertTypeCounts"
:helper-text="t('certd.dashboard.savedAmount')"
:helper-value="`¥${formatSavedAmount(count.totalSavedAmount)}`"
:helper-tooltip="t('certd.dashboard.savedAmountRule')"
:footer="false"
/>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:link-multiple-24" :title="t('certd.dashboard.siteMonitorCount')" :count="count.siteCount" link="/cert/monitor/site" :sub-counts="count.siteStatusCount">
<template #footer>
<router-link to="/cert/monitor/site" class="flex"> <fs-icon icon="ion:settings-outline" class="mr-5 fs-16" /> {{ t("certd.dashboard.manageSiteMonitor") }} </router-link>
</template>
</statistic-card>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:alert-urgent-24" :title="t('certd.dashboard.expiringCerts')">
<expiring-list v-if="count.expiringList" :data="count.expiringList"></expiring-list>
<statistic-card icon="fluent-color:globe-24" :title="t('certd.dashboard.domainCount')" :count="count.domainCount" link="/cert/cert/domain" :sub-counts="count.domainStatusCount">
<template #footer>
<router-link to="/cert/cert/domain" class="flex"> <fs-icon icon="ion:settings-outline" class="mr-5 fs-16" /> {{ t("certd.dashboard.manageDomain") }} </router-link>
</template>
</statistic-card>
</a-col>
</a-row>
@@ -121,6 +148,7 @@
<fs-icon icon="fluent-color:puzzle-piece-24" class="mr-5 fs-28" />
<div class="mr-5">{{ t("certd.dashboard.supportedTasks") }}</div>
<a-tag color="green">{{ pluginGroups.groups.all.plugins.length }}</a-tag>
<div class="helper">列表仅做展示,请创建流水线来使用它们</div>
</div>
</template>
<a-row :gutter="10">
@@ -159,6 +187,7 @@ import { useRouter } from "vue-router";
import * as api from "./api";
import DayCount from "./charts/day-count.vue";
import ExpiringList from "./charts/expiring-list.vue";
import PieCount from "./charts/pie-count.vue";
import NoticeBar from "./notice-bar.vue";
import SuiteCard from "./suite-card.vue";
import TutorialButton from "/@/components/tutorial/index.vue";
@@ -260,6 +289,10 @@ function goPipeline() {
}
const count: any = ref({});
function formatSavedAmount(amountValue: number) {
const amount = Number(amountValue) || 0;
return Number.isInteger(amount) ? amount.toString() : amount.toFixed(2);
}
function transformStatusCount() {
const data = count.value.pipelineStatusCount;
const sorted = [
@@ -300,6 +333,33 @@ function transformStatusCount() {
{ name: t("certd.dashboard.certNoExpireCount"), value: certCount.notExpired, color: "green", link: { path: "/cert/monitor/cert", query: { expireStatus: "noExpired" } } },
];
count.value.certCount = certCount.total;
const siteCount = count.value.siteCount;
count.value.siteStatusCount = [
{ name: t("certd.dashboard.siteAbnormalCount"), value: siteCount.abnormal, color: "red", checkIcon: "mingcute:warning-fill:#f44336", link: { path: "/cert/monitor/site", query: { checkStatus: "error" } } },
{ name: t("certd.dashboard.siteNormalCount"), value: siteCount.normal, color: "green" },
];
count.value.siteCount = siteCount.total;
const domainCount = count.value.domainCount;
count.value.domainStatusCount = [
{ name: t("certd.dashboard.domainExpiredCount"), value: domainCount.expired, color: "red", checkIcon: "mingcute:warning-fill:#f44336", link: { path: "/cert/cert/domain", query: { expirationStatus: "expired" } } },
{ name: t("certd.dashboard.domainExpiringCount"), value: domainCount.expiring, color: "yellow", checkIcon: "mingcute:alert-fill:#ff9800" },
{ name: t("certd.dashboard.domainNotExpiredCount"), value: domainCount.notExpired, color: "green" },
];
count.value.domainCount = domainCount.total;
const genCertCount = count.value.genCertCount || {};
const singleDomainCertCount = Number(genCertCount.singleDomainCertCount) || 0;
const multiDomainCertCount = Number(genCertCount.multiDomainCertCount) || 0;
const wildcardCertCount = Number(genCertCount.wildcardCertCount) || 0;
count.value.totalGeneratedCertCount = singleDomainCertCount + multiDomainCertCount + wildcardCertCount;
count.value.generatedCertTypeCounts = [
{ name: t("certd.dashboard.singleDomainCertShortCount"), value: singleDomainCertCount, color: "blue" },
{ name: t("certd.dashboard.multiDomainCertShortCount"), value: multiDomainCertCount, color: "blue" },
{ name: t("certd.dashboard.wildcardCertShortCount"), value: wildcardCertCount, color: "blue" },
];
count.value.totalSavedAmount = singleDomainCertCount * (532 / 4) + multiDomainCertCount * (912 / 4) + wildcardCertCount * (2147 / 4);
}
async function loadCount() {
count.value = await GetStatisticCount();
@@ -1,6 +1,6 @@
<template>
<div class="statistic-card">
<a-card>
<a-card :class="cardTone">
<div class="data-item">
<div class="header">
<div class="title">
@@ -11,9 +11,9 @@
</div>
<div class="content">
<div v-if="!slots.default" class="statistic">
<div v-if="count !== 0" class="value flex items-center w-full">
<div class="value flex items-center w-full">
<div class="total flex-center flex-1 flex-col pointer" @click="goDetail(link)">
<span>{{ count }}</span>
<span>{{ count ?? 0 }}</span>
<span class="sub-title">{{ title }}</span>
</div>
<a-divider type="vertical h-10"></a-divider>
@@ -32,7 +32,13 @@
</div>
</div>
</div>
<a-empty v-else></a-empty>
<div v-if="helperText" class="helper-text mt-10">
<span>{{ helperText }}</span>
<strong v-if="helperValue" class="helper-value">{{ helperValue }}</strong>
<a-tooltip v-if="helperTooltip" :title="helperTooltip">
<fs-icon icon="mingcute:information-line" class="helper-icon" />
</a-tooltip>
</div>
</div>
<slot></slot>
</div>
@@ -45,6 +51,7 @@
</template>
<script setup lang="ts">
import { FsIcon } from "@fast-crud/fast-crud";
import { computed } from "vue";
import { useRouter } from "vue-router";
const props = defineProps<{
icon: string;
@@ -59,9 +66,21 @@ const props = defineProps<{
title?: string;
link?: any;
}[];
helperText?: string;
helperValue?: string;
helperTooltip?: string;
}>();
const slots = defineSlots();
const router = useRouter();
const cardTone = computed(() => {
if (props.subCounts?.some(item => item.color === "red" && item.value > 0)) {
return "has-danger";
}
if (props.subCounts?.some(item => item.color === "yellow" && item.value > 0)) {
return "has-warning";
}
return "";
});
function goDetail(link: any) {
if (!link) {
return;
@@ -80,9 +99,35 @@ function goDetail(link: any) {
color: rgba(242, 242, 242, 0.85) !important;
}
}
.statistic-card {
.ant-card.has-danger {
background: linear-gradient(135deg, rgba(92, 35, 35, 0.72), rgba(28, 28, 28, 0.96) 62%);
}
.ant-card.has-warning {
background: linear-gradient(135deg, rgba(91, 72, 25, 0.68), rgba(28, 28, 28, 0.96) 62%);
}
.helper-text {
color: rgba(242, 242, 242, 0.72);
.helper-value {
color: #8bd7a0;
}
}
}
}
.statistic-card {
margin-bottom: 10px;
.ant-card.has-danger {
background: linear-gradient(135deg, rgba(255, 241, 240, 0.9), #fff 58%);
}
.ant-card.has-warning {
background: linear-gradient(135deg, rgba(255, 251, 230, 0.9), #fff 58%);
}
.icon-text {
display: inline-flex;
justify-content: left;
@@ -143,6 +188,7 @@ function goDetail(link: any) {
font-weight: 700;
.total {
color: hsl(var(--primary));
.sub-title {
font-size: 12px;
font-weight: 400;
@@ -180,6 +226,31 @@ function goDetail(link: any) {
}
}
}
.helper-text {
display: flex;
align-items: baseline;
justify-content: center;
gap: 4px;
margin-top: 6px;
color: #626262;
font-size: 12px;
font-weight: 400;
line-height: 20px;
.helper-value {
color: #52a26a;
font-size: 16px;
font-weight: 700;
}
.helper-icon {
cursor: help;
font-size: 14px;
margin-left: 2px;
vertical-align: -2px;
}
}
}
}
@@ -1,3 +1,4 @@
import { PluginDefine } from "@certd/pipeline";
import { request } from "/src/api/service";
const apiPrefix = "/sys/plugin";
@@ -108,6 +109,7 @@ export type OnlinePluginBean = {
desc?: string;
latest?: string;
status?: string;
email?: string;
downloadCount?: number;
score?: number;
aiCheckStatus?: string;
@@ -218,14 +220,8 @@ export async function OnlinePluginPublishInfo(body: { id: number }): Promise<{
export type OnlinePluginAuthorBean = {
id?: number;
appId?: number;
appOwnerId?: number;
developerId?: number;
name?: string;
displayName?: string;
avatar?: string;
desc?: string;
status?: string;
email?: string;
};
export async function OnlinePluginAuthorGet(options?: { showErrorNotify?: boolean }): Promise<{ registered?: boolean; author?: OnlinePluginAuthorBean }> {
@@ -236,18 +232,22 @@ export async function OnlinePluginAuthorGet(options?: { showErrorNotify?: boolea
});
}
export async function OnlinePluginAuthorAdd(body: { name: string; displayName?: string; avatar?: string; desc?: string }): Promise<OnlinePluginAuthorBean> {
export async function OnlinePluginAuthorAdd(body: { name: string; email: string }): Promise<OnlinePluginAuthorBean> {
return await request({
url: apiPrefix + "/online/author/add",
method: "post",
data: body,
});
}
export async function OnlinePluginAuthorUpdate(body: { email: string }): Promise<OnlinePluginAuthorBean> {
return await request({ url: apiPrefix + "/online/author/update", method: "post", data: body });
}
export type PluginConfigBean = {
name: string;
disabled: boolean;
sysSetting: {
metadata?: Record<string, any>;
input?: Record<string, any>;
};
};
@@ -285,7 +285,7 @@ export async function SaveCommPluginConfigs(data: CommPluginConfig): Promise<voi
});
}
export async function savePluginSetting(req: { name: string; sysSetting: any }): Promise<void> {
export async function savePluginSetting(req: { name: string; sysSetting: any; type: string }): Promise<void> {
return await request({
url: apiPrefix + "/saveSetting",
method: "post",
@@ -293,11 +293,11 @@ export async function savePluginSetting(req: { name: string; sysSetting: any }):
});
}
export async function GetPluginByName(name: string): Promise<PluginConfigBean> {
export async function getPluginSetting(name: string, type: string = "builtIn"): Promise<PluginConfigBean> {
return await request({
url: apiPrefix + "/getPluginByName",
url: apiPrefix + "/getSetting",
method: "post",
data: { name },
data: { name, type },
});
}
@@ -307,3 +307,11 @@ export async function ClearRuntimeDeps(): Promise<void> {
method: "post",
});
}
export async function getPluginDefine(name: string): Promise<PluginDefine> {
return await request({
url: apiPrefix + "/getPluginDefine",
method: "post",
data: { name },
});
}
@@ -1,7 +1,9 @@
<template>
<div class="plugin-author-field">
<a-input :value="modelValue" placeholder="请输入作者名称" @update:value="emit('update:modelValue', $event)" />
<a-button type="link" size="small" :loading="loading" @click="registerAuthor">注册作者</a-button>
<a-input :value="modelValue" placeholder="请输入作者名称" :disabled="authorRegistered" @update:value="emit('update:modelValue', $event)" />
<a-button type="link" size="small" :loading="loading" @click="handleAuthorAction">
{{ authorRegistered ? "修改邮箱" : "注册作者" }}
</a-button>
</div>
</template>
@@ -15,7 +17,9 @@ defineOptions({ name: "PluginAuthorField" });
const props = defineProps<{ modelValue?: string }>();
const emit = defineEmits<{ (event: "update:modelValue", value: string): void }>();
const loading = ref(false);
const { registerPluginAuthor } = usePluginPublish();
const { registerPluginAuthor, updatePluginAuthorEmail } = usePluginPublish();
const registeredAuthor = ref<api.OnlinePluginAuthorBean>();
const authorRegistered = computed(() => !!registeredAuthor.value?.id);
const isLocalAuthor = computed(() => {
return (
String(props.modelValue || "")
@@ -28,7 +32,8 @@ async function loadAuthor() {
loading.value = true;
try {
const result = await api.OnlinePluginAuthorGet({ showErrorNotify: false });
if ((!props.modelValue || isLocalAuthor.value) && result.author?.name) {
if (result.registered && result.author?.id && result.author.name) {
registeredAuthor.value = result.author;
emit("update:modelValue", result.author.name);
}
} catch {
@@ -38,11 +43,19 @@ async function loadAuthor() {
}
}
async function registerAuthor() {
async function handleAuthorAction() {
if (registeredAuthor.value) {
const author = await updatePluginAuthorEmail(registeredAuthor.value);
if (author?.id) {
registeredAuthor.value = author;
}
return;
}
const author = await registerPluginAuthor();
if (!author?.name) {
return;
}
registeredAuthor.value = author;
emit("update:modelValue", author.name);
}
@@ -105,6 +105,9 @@ async function loadPlugin() {
if (baseForm.extra) {
baseForm.extra = yaml.load(baseForm.extra);
}
if (baseForm.dependPlugins) {
baseForm.dependPlugins = yaml.load(baseForm.dependPlugins);
}
delete baseForm.metadata;
delete baseForm.content;
baseFormRef.value.setFormData(baseForm);
@@ -136,6 +139,9 @@ function buildSubmitForm(versionOverride?: string) {
if (form.extra) {
form.extra = yaml.dump(form.extra);
}
if (form.dependPlugins) {
form.dependPlugins = yaml.dump(form.dependPlugins);
}
return form;
}
@@ -29,7 +29,7 @@
</div>
</div>
<div class="plugin-card__actions">
<div v-if="pluginCardState.showEditButton || pluginCardState.showConfigButton" class="plugin-card__tools">
<div v-if="pluginCardState.showEditButton || pluginCardState.showConfigButton || pluginCardState.canExportPlugin" class="plugin-card__tools">
<a-tooltip v-if="pluginCardState.showEditButton" title="编辑">
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="editPlugin">
<template #icon>
@@ -37,10 +37,10 @@
</template>
</a-button>
</a-tooltip>
<a-tooltip v-if="copyHandler && pluginCardState.canCopyPlugin" title="复制">
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="copyPlugin">
<a-tooltip v-if="pluginCardState.canExportPlugin" :title="t('certd.export')">
<a-button class="plugin-card__tool" type="text" size="small" :loading="isActionLoading('export')" @click.stop="exportPlugin">
<template #icon>
<fs-icon icon="ion:copy-outline" />
<fs-icon icon="ion:cloud-download-outline" />
</template>
</a-button>
</a-tooltip>
@@ -89,7 +89,7 @@
<div class="plugin-card__meta">
<template v-if="source === 'local'">
<a-tooltip :title="toggleTitle">
<a-tooltip v-if="plugin.showDisabled" :title="toggleTitle">
<a-tag class="plugin-status-tag" :class="{ 'is-loading': isActionLoading('toggle') }" :color="plugin.disabled ? 'default' : 'green'" @click.stop="togglePlugin">
{{ plugin.disabled ? t("certd.onlinePluginDisabled") : t("certd.onlinePluginEnabled") }}
</a-tag>
@@ -99,7 +99,7 @@
<a-tag v-if="plugin.group">{{ plugin.group }}</a-tag>
</template>
<template v-else-if="!simple">
<a-tooltip v-if="plugin.installed && plugin.id" :title="toggleTitle">
<a-tooltip v-if="plugin.installed && plugin.id && plugin.showDisabled" :title="toggleTitle">
<a-tag class="plugin-status-tag" :class="{ 'is-loading': isActionLoading('toggle') }" :color="plugin.disabled ? 'default' : 'green'" @click.stop="togglePlugin">
{{ plugin.disabled ? t("certd.onlinePluginDisabled") : t("certd.onlinePluginEnabled") }}
</a-tag>
@@ -147,6 +147,7 @@ import PluginEditDialogBody from "./plugin-edit-dialog-body.vue";
import { usePluginStore } from "/@/store/plugin";
import { useFormDialog } from "/@/use/use-dialog";
import { useI18n } from "/src/locales";
import { useSettingStore } from "/@/store/settings";
defineOptions({
name: "PluginItemCard",
@@ -160,11 +161,9 @@ const props = withDefaults(
simple?: boolean;
showConfig?: boolean;
editable?: boolean;
copyHandler?: (plugin: any) => void | Promise<void>;
}>(),
{
source: "market",
copyHandler: undefined,
}
);
@@ -183,8 +182,9 @@ const { resolveOnlinePluginDependencies, installOnlinePluginChain, openDependenc
const actionLoading = ref<PluginCardAction | "">("");
const editDialogBodyRef = ref();
type PluginCardAction = "edit" | "copy" | "publish" | "config" | "install" | "uninstall" | "remove" | "toggle";
type PluginCardAction = "edit" | "export" | "publish" | "config" | "install" | "uninstall" | "remove" | "toggle";
const settingStore = useSettingStore();
const pluginCardState = computed(() => {
const isLocal = props.source === "local";
const isCloudPlugin = Number(props.plugin.developerId) > 0;
@@ -192,7 +192,6 @@ const pluginCardState = computed(() => {
const isAvailableLocally = isLocal || !!props.plugin.installed;
const canEditPlugin = props.editable === true;
const canManagePlugin = !isBuiltInPlugin && canEditPlugin && isAvailableLocally;
return {
editPluginId: props.plugin.id,
isLocal,
@@ -200,11 +199,12 @@ const pluginCardState = computed(() => {
canEditPlugin,
isAvailableLocally,
canManagePlugin,
canCopyPlugin: canManagePlugin,
canExportPlugin: canManagePlugin,
showEditButton: canManagePlugin,
showConfigButton: !!props.showConfig,
showConfigButton: !!props.showConfig && isAvailableLocally && settingStore.isComm,
isInstalled: isCloudPlugin && !!props.plugin.installed,
isDisabled: !!props.plugin.disabled,
showDisabled: settingStore.isComm,
requiresVip: props.plugin.vip === "plus" || props.plugin.needPlus === true,
canRemovePlugin: isLocal && canManagePlugin,
isBuiltInPlugin,
@@ -354,16 +354,26 @@ async function editPlugin() {
});
}
async function copyPlugin() {
if (!props.copyHandler) {
async function exportPlugin() {
if (!pluginCardState.value.canExportPlugin || !props.plugin.id) {
return;
}
await runAction(
"copy",
"export",
async () => {
await props.copyHandler?.(props.plugin);
const content = await api.ExportPlugin(props.plugin.id);
if (!content) {
return;
}
const blob = new Blob([content], { type: "text/yaml;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${props.plugin.name || props.plugin.fullName || "plugin"}.yaml`;
link.click();
URL.revokeObjectURL(url);
},
{ emitChanged: true }
{ emitChanged: false }
);
}
@@ -1052,5 +1062,4 @@ function handleVersionClick() {
}
}
}
</style>
@@ -69,7 +69,7 @@
<script lang="ts" setup>
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import { reactive, ref } from "vue";
import { CommPluginConfig, GetCommPluginConfigs, SaveCommPluginConfigs, GetPluginByName } from "/@/views/sys/plugin/api";
import { CommPluginConfig, GetCommPluginConfigs, SaveCommPluginConfigs, getPluginSetting } from "/@/views/sys/plugin/api";
import { merge } from "lodash-es";
import { notification } from "ant-design-vue";
import { usePluginConfig } from "./use-config";
@@ -113,7 +113,8 @@ const onFinishFailed = (errorInfo: any) => {
const { openConfigDialog } = usePluginConfig();
async function doPluginConfig() {
const certApplyInfo = await GetPluginByName("CertApply");
const certApplyInfo: any = await getPluginSetting("CertApply");
certApplyInfo.pluginType = "plugin";
await openConfigDialog({
row: certApplyInfo,
onSuccess: async () => {
@@ -45,10 +45,10 @@
<script setup lang="tsx">
import { dict, FsRender } from "@fast-crud/fast-crud";
import { cloneDeep, merge, unset } from "lodash-es";
import { computed, onMounted, reactive, ref } from "vue";
import { computed, onMounted, provide, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import Rollbackable from "./rollbackable.vue";
import { usePluginStore } from "/@/store/plugin";
import { fillPluginDefine, usePluginStore } from "/@/store/plugin";
import * as api from "./api";
// @ts-ignore js-yaml 没有在当前前端包中提供类型声明。
import yaml from "js-yaml";
@@ -106,7 +106,7 @@ const editableKeys = ref([
},
editRender(item: any) {
return () => {
return <fs-component-render {...item.component} vModel:modelValue={configForm[item.key]["value"]} scope={getScope()} />;
return <fs-component-render {...item.component} vModel:modelValue={configForm[item.key].value} scope={getScope()} />;
};
},
},
@@ -185,9 +185,11 @@ const optionsMappingKey = {
},
editRender(item: any) {
return () => {
const options = item.component?.options || [];
if (options.length === 0) {
return <span class="text-gray-400">该组件没有预设选项</span>;
const options = reactive(cloneDeep(item.component?.options || []));
const showAdd = options.length === 0;
function addOption(opt: any) {
options.push(opt);
}
const onLabelChange = (optValue: string, newLabel: string) => {
@@ -233,6 +235,13 @@ const optionsMappingKey = {
))}
</tbody>
</table>
{showAdd && (
<div class="mt-2">
<a-button type="primary" size={() => addOption({ value: "", label: "" })}>
添加选项
</a-button>
</div>
)}
<div class="helper mt-1">只需填写需要自定义的选项,留空则使用原始显示内容</div>
</div>
);
@@ -267,38 +276,18 @@ function clearFormValue(key: string) {
console.log(key, configForm);
}
function getPluginOriginName() {
if (props.plugin.fullName) {
return props.plugin.fullName;
}
if (props.plugin.author && props.plugin.name) {
return `${props.plugin.author}/${props.plugin.name}`;
}
return props.plugin.name;
}
async function loadPluginSetting() {
const originName = getPluginOriginName();
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(originName);
if (!currentPlugin.value?.input && originName !== props.plugin.name) {
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(props.plugin.name);
}
if (!currentPlugin.value?.input && props.plugin.id) {
const plugin = await api.GetObj(props.plugin.id);
const metadata = plugin.metadata ? yaml.load(plugin.metadata) : {};
currentPlugin.value = {
...plugin,
...(metadata || {}),
};
}
const pluginName = props.plugin.fullName || props.plugin.name; // 这里必须两种进行兼容
currentPlugin.value = await api.getPluginDefine(pluginName);
fillPluginDefine(currentPlugin.value);
const input = currentPlugin.value?.input || {};
for (const key in input) {
configForm[key] = {};
}
const setting = currentPlugin.value?.sysSetting || props.plugin.sysSetting;
if (setting) {
const settingJson = JSON.parse(setting);
merge(configForm, settingJson.metadata?.input || {});
const pluginConfig = await api.getPluginSetting(pluginName, currentPlugin.value.type);
if (pluginConfig?.sysSetting) {
merge(configForm, pluginConfig.sysSetting?.metadata?.input || {});
}
}
@@ -309,6 +298,16 @@ onMounted(async () => {
defineExpose({
getForm,
});
provide("getCurrentPluginDefine", () => {
return { value: props.plugin };
});
provide("get:scope", () => {
return getScope();
});
provide("get:plugin:type", () => {
return props.plugin.pluginType;
});
</script>
<style lang="less">
@@ -383,7 +383,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
{ label: t("certd.dns"), value: "dnsProvider" },
{ label: t("certd.deployPlugin"), value: "deploy" },
{ label: "通知", value: "notification" },
{ label: "Addon", value: "addon" },
// { label: "Addon", value: "addon" },
],
}),
column: {
@@ -463,6 +463,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
name: PluginAuthorField,
vModel: "modelValue",
},
helper: "为保证插件名称引用一致性,作者一旦注册不允许修改",
rules: [{ required: true, message: "请先注册并选择插件作者" }],
},
column: {
@@ -613,6 +614,16 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
column: {
show: false,
},
valueBuilder({ row }) {
if (typeof row.dependPlugins === "string") {
row.dependPlugins = yaml.load(row.dependPlugins);
}
},
valueResolve({ row }) {
if (row.dependPlugins && typeof row.dependPlugins === "object") {
row.dependPlugins = yaml.dump(row.dependPlugins);
}
},
},
"extra.dependPackages": {
title: t("certd.thirdPartyDependencies"),
@@ -9,17 +9,7 @@
<fs-crud ref="crudRef" class="plugin-card-crud" v-bind="crudBinding">
<a-empty v-if="pluginList.length === 0" class="plugin-card-empty" />
<div v-else class="plugin-card-grid">
<PluginItemCard
v-for="item of pluginList"
:key="item.fullName"
:source="getPluginCardSource(item)"
:plugin="item"
show-config
:editable="item.editable"
:copy-handler="copyPlugin"
@changed="handlePluginChanged"
@click="openPluginDetail"
/>
<PluginItemCard v-for="item of pluginList" :key="item.fullName" :source="getPluginCardSource(item)" :plugin="item" show-config :editable="item.editable" @changed="handlePluginChanged" @click="openPluginDetail" />
</div>
</fs-crud>
</fs-page>
@@ -80,30 +70,8 @@ async function handleDetailInstalled() {
crudExpose.doRefresh();
}
async function copyPlugin(row: any) {
const copyRow = { ...row };
delete copyRow.fullName;
delete copyRow.id;
delete copyRow.developerId;
delete copyRow.appId;
delete copyRow.latest;
delete copyRow.status;
delete copyRow.downloadCount;
delete copyRow.score;
await crudExpose.openCopy(
{
row: copyRow,
},
{
async onSuccess() {
crudExpose.doRefresh();
},
}
);
}
async function handlePluginChanged(payload: { action: string }) {
if (payload.action === "install" || payload.action === "uninstall" || payload.action === "remove" || payload.action === "copy") {
if (payload.action === "install" || payload.action === "uninstall" || payload.action === "remove") {
await pluginStore.reload();
}
crudExpose.doRefresh();
@@ -49,12 +49,13 @@ export function usePluginConfig() {
}
}
const res = await api.savePluginSetting({
name: row.name,
name: row.fullName || row.name,
sysSetting: {
metadata: {
input: newForm,
},
},
type: row.type,
});
await pluginStore.clear();
return res;
@@ -14,6 +14,9 @@ type PublishManagerResult =
| {
action: "registered";
}
| {
action: "updated";
}
| {
action: "cancel";
};
@@ -61,7 +64,7 @@ export function usePluginPublish() {
initialForm: {
name: "",
nameConfirm: "",
desc: "",
email: "",
},
async onSubmit(form: any) {
if (form.name?.trim() !== form.nameConfirm?.trim()) {
@@ -72,8 +75,7 @@ export function usePluginPublish() {
}
const author = await api.OnlinePluginAuthorAdd({
name: form.name,
displayName: form.displayName,
desc: form.desc,
email: form.email,
});
resolveAuthor(author);
},
@@ -111,6 +113,51 @@ export function usePluginPublish() {
],
},
},
email: {
title: "作者邮箱",
type: "text",
form: {
col: { span: 24 },
rules: [
{ required: true, message: "请输入作者邮箱" },
{ type: "email", message: "请输入有效的作者邮箱" },
],
},
},
},
});
});
}
function updatePluginAuthorEmail(author?: api.OnlinePluginAuthorBean) {
return new Promise<api.OnlinePluginAuthorBean | undefined>(async resolve => {
let resolved = false;
const resolveAuthor = (result?: api.OnlinePluginAuthorBean) => {
if (!resolved) {
resolved = true;
resolve(result);
}
};
await openFormDialog({
title: "修改作者邮箱",
wrapper: { width: 560, onClosed: () => resolveAuthor() },
initialForm: { email: author?.email || "" },
async onSubmit(form: any) {
const result = await api.OnlinePluginAuthorUpdate({ email: form.email });
resolveAuthor(result);
},
columns: {
email: {
title: "作者邮箱",
type: "text",
form: {
col: { span: 24 },
rules: [
{ required: true, message: "请输入作者邮箱" },
{ type: "email", message: "请输入有效的作者邮箱" },
],
},
},
},
});
});
@@ -326,7 +373,26 @@ export function usePluginPublish() {
function renderAuthor() {
if (info.authorRegistered) {
return author?.displayName || author?.name || "-";
return (
<span class="sys-plugin-publish__author-register">
<span>{author?.name || "-"}</span>
<a-button
size="small"
type="link"
onClick={async (event: any) => {
event.stopPropagation();
const updatedAuthor = await updatePluginAuthorEmail(author);
if (!updatedAuthor?.id) {
return;
}
resolveResult({ action: "updated" });
formWrapper?.close?.();
}}
>
修改邮箱
</a-button>
</span>
);
}
return (
<span class="sys-plugin-publish__author-register">
@@ -490,6 +556,10 @@ export function usePluginPublish() {
await publishLocalPlugin(row, options);
return;
}
if (action.action === "updated") {
await publishLocalPlugin(row, options);
return;
}
if (action.action !== "publish") {
return;
}
@@ -521,10 +591,10 @@ export function usePluginPublish() {
publishingPluginId.value = "";
}
}
return {
isPublishingPlugin,
publishLocalPlugin,
registerPluginAuthor,
updatePluginAuthorEmail,
};
}
@@ -3,7 +3,7 @@
<template #header>
<div class="title">
套餐设置
<span class="sub"> 需要<router-link :to="{ path: '/sys/settings', query: { tab: 'payment' } }">开启至少一种支付方式</router-link></span>
<span class="sub"> 建议<router-link :to="{ path: '/sys/settings', query: { tab: 'payment' } }">开启至少一种支付方式</router-link></span>
</div>
</template>
@@ -38,7 +38,7 @@
<a-form-item label=" " :colon="false">
<loading-button type="primary" html-type="button" :click="onClick">保存</loading-button>
<div class="helper">需要 <router-link :to="{ path: '/sys/settings', query: { tab: 'payment' } }">开启至少一种支付方式</router-link></div>
<div class="helper">建议 <router-link :to="{ path: '/sys/settings', query: { tab: 'payment' } }">开启至少一种支付方式</router-link></div>
</a-form-item>
</a-form>
</div>
@@ -0,0 +1,35 @@
import { request } from "/src/api/service";
const apiPrefix = "/sys/wallet";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
}
export async function Recharge(data: { userId: number; amount: number; remark?: string }) {
return await request({
url: apiPrefix + "/recharge",
method: "post",
data,
});
}
export async function GetWalletLogs(query: any) {
return await request({
url: apiPrefix + "/log/page",
method: "post",
data: query,
});
}
export async function GetSimpleUserByIds(ids: number[]) {
return await request({
url: "/sys/authority/user/getSimpleUserByIds",
method: "post",
data: { ids },
});
}
@@ -0,0 +1,193 @@
import { CreateCrudOptionsProps, CreateCrudOptionsRet, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { notification } from "ant-design-vue";
import { useRouter } from "vue-router";
import { util } from "/@/utils";
import { useFormDialog } from "/@/use/use-dialog";
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
import * as api from "./api";
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { openFormDialog } = useFormDialog();
const router = useRouter();
const userDict = dict({
async getNodesByValues(ids: number[]) {
return await api.GetSimpleUserByIds(ids);
},
value: "id",
label: "nickName",
});
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
function moneyColumn(title: string) {
return {
title,
type: "number",
column: {
width: 120,
component: {
name: PriceInput,
vModel: "modelValue",
edit: false,
},
},
};
}
async function openRecharge(userId?: number) {
await openFormDialog({
title: "用户余额充值",
wrapper: { width: 520 },
initialForm: {
userId: userId ?? null,
amountYuan: null,
remark: "",
},
columns: {
userId: {
title: "用户",
type: "table-select",
dict: userDict,
form: {
col: { span: 24 },
rules: [{ required: true, message: "请选择用户" }],
component: {
disabled: userId != null,
crossPage: true,
multiple: false,
select: {
placeholder: "请选择用户",
},
createCrudOptions: createCrudOptionsUser,
},
},
},
amountYuan: {
title: "充值金额",
type: "number",
form: {
col: { span: 24 },
rules: [{ required: true, message: "请输入充值金额" }],
component: {
name: "a-input-number",
vModel: "value",
min: 0,
precision: 2,
addonAfter: "元",
style: { width: "100%" },
},
},
},
remark: {
title: "备注",
type: "text",
form: {
col: { span: 24 },
helper: "选填",
},
},
},
async onSubmit(form: any) {
await api.Recharge({
userId: form.userId,
amount: util.amount.toCent(form.amountYuan || 0),
remark: form.remark,
});
await crudExpose.doRefresh();
notification.success({ message: "充值成功" });
},
});
}
return {
crudOptions: {
request: {
pageRequest,
},
toolbar: { show: false },
actionbar: {
buttons: {
add: { show: false },
recharge: {
text: "余额充值",
type: "primary",
click: () => openRecharge(),
},
log: {
text: "余额变更记录",
type: "primary",
click: () => router.push("/sys/suite/wallet-log"),
},
},
},
rowHandle: {
width: 220,
fixed: "right",
buttons: {
view: { show: false },
edit: { show: false },
copy: { show: false },
remove: { show: false },
walletLog: {
text: "余额变更记录",
type: "link",
click: ({ row }) => router.push({ path: "/sys/suite/wallet-log", query: { userId: row.userId } }),
},
recharge: {
text: "充值",
type: "link",
click: ({ row }) => openRecharge(row.userId),
},
},
},
columns: {
id: {
title: "ID",
type: "number",
column: { width: 80 },
form: { show: false },
},
userId: {
title: "用户",
type: "table-select",
dict: userDict,
search: { show: true, col: { span: 3 } },
column: { width: 160 },
form: {
show: false,
component: {
crossPage: true,
multiple: false,
select: {
placeholder: "点击选择用户",
},
createCrudOptions: createCrudOptionsUser,
},
},
},
availableAmount: moneyColumn("可用余额"),
totalAmount: moneyColumn("总余额"),
frozenAmount: moneyColumn("冻结余额"),
totalIncomeAmount: moneyColumn("累计收入"),
totalConsumedAmount: moneyColumn("累计消费"),
totalWithdrawAmount: moneyColumn("累计提现"),
createTime: {
title: "创建时间",
type: "datetime",
form: { show: false },
column: { sorter: true, width: 160 },
},
updateTime: {
title: "更新时间",
type: "datetime",
form: { show: false },
column: { width: 160 },
},
},
},
};
}
@@ -0,0 +1,28 @@
<template>
<fs-page>
<template #header>
<div class="title">
余额管理
<span class="sub">管理用户余额,可给用户充值</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
defineOptions({
name: "SysWallet",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
useMounted(async () => {
await crudExpose.doRefresh();
});
</script>
@@ -0,0 +1,100 @@
import { CreateCrudOptionsRet, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useRoute } from "vue-router";
import { util } from "/@/utils";
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
import * as api from "../api";
function moneyText(amount: number) {
const yuan = util.amount.toYuan(Math.abs(amount || 0));
if (amount < 0) {
return `-¥${yuan}`;
}
return `¥${yuan}`;
}
export default function (): CreateCrudOptionsRet {
const route = useRoute();
const userIdFromQuery = route.query.userId ? Number(route.query.userId) : undefined;
const userDict = dict({
async getNodesByValues(ids: number[]) {
return await api.GetSimpleUserByIds(ids);
},
value: "id",
label: "nickName",
});
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetWalletLogs(query);
};
return {
crudOptions: {
request: { pageRequest },
search: {
initialForm: { userId: userIdFromQuery },
},
actionbar: { show: false },
toolbar: { show: false },
rowHandle: { show: false },
columns: {
createTime: { title: "时间", type: "datetime", column: { width: 180 } },
userId: {
title: "用户",
type: "table-select",
dict: userDict,
search: { show: true, col: { span: 3 } },
column: { width: 160 },
form: {
show: false,
component: {
crossPage: true,
multiple: false,
select: { placeholder: "点击选择用户" },
createCrudOptions: createCrudOptionsUser,
},
},
},
type: {
title: "类型",
type: "dict-select",
dict: dict({
data: [
{ label: "收益入账", value: "income", color: "success" },
{ label: "余额抵扣", value: "consume", color: "default" },
{ label: "提现扣减", value: "withdraw", color: "warning" },
],
}),
search: { show: true, col: { span: 3 } },
column: { width: 120 },
},
amount: {
title: "变动金额",
type: "number",
column: {
width: 120,
cellRender({ value }) {
const amount = Number(value || 0);
return <span class={amount < 0 ? "text-green-500" : "text-red-500"}>{moneyText(amount)}</span>;
},
},
},
balanceAfter: {
title: "变动后余额",
type: "number",
column: {
width: 130,
cellRender({ value }) {
return <span class="text-red-500">{moneyText(Number(value || 0))}</span>;
},
},
},
remark: {
title: "备注",
type: "text",
column: { minWidth: 220 },
},
},
},
};
}
@@ -0,0 +1,45 @@
<template>
<fs-page>
<template #header>
<div class="title">
{{ pageTitle }}
<span class="sub">{{ pageSub }}</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useRoute } from "vue-router";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
defineOptions({
name: "SysWalletLog",
});
const route = useRoute();
const isUserLog = computed(() => route.query.userId != null);
const pageTitle = computed(() => {
if (isUserLog.value) {
return "用户余额变更记录";
}
return "余额变更记录";
});
const pageSub = computed(() => {
if (isUserLog.value) {
return "查看该用户的余额变更明细";
}
return "查看所有用户的余额变更明细";
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
useMounted(async () => {
await crudExpose.doRefresh();
});
</script>
+39
View File
@@ -3,6 +3,45 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Performance Improvements
* 新增余额管理,管理员充值功能 ([bded368](https://github.com/certd/certd/commit/bded368cbb8974b6937ae260e448c25fae5c33ad))
* **acme-account-access:** AcmeAccount授权允许修改eab,重新生成account ([6a649aa](https://github.com/certd/certd/commit/6a649aaca083ccd3b517f7d676a58220aa33b37d))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
### Bug Fixes
* **plugin-service:** 修复同步自己开发的云端插件不显示已安装状态问题 ([dac94b6](https://github.com/certd/certd/commit/dac94b64f0ceb7176e2cf07ce220be0aa5ed98d2))
* **plugin:** 修复插件配置保存与加载的相关问题 ([bd94334](https://github.com/certd/certd/commit/bd9433478a43d5d64ce6370386d0fe0aaff07d2e))
### Performance Improvements
* **plugin:** add Fastly custom TLS certificates integration ([#797](https://github.com/certd/certd/issues/797)) [@szhu25](https://github.com/szhu25) ([2a2e963](https://github.com/certd/certd/commit/2a2e963914c4dfc192cbe0a4bbb9c115e626da01))
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
### Bug Fixes
* 修复插件市场插件安装无效的bug ([15d2bd8](https://github.com/certd/certd/commit/15d2bd8484a9ddd142bf4ef03d839c74405f9e6d))
* preserve Route53 TXT record values ([#791](https://github.com/certd/certd/issues/791)) ([f5430a8](https://github.com/certd/certd/commit/f5430a8925cf6cf4e16c75f2358fbd78437e6705))
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
### Bug Fixes
* 修复编辑证书申请任务导致清空ACME账号的bug ([d67379f](https://github.com/certd/certd/commit/d67379f29725bbd6dcdefe97d101023c7fd11f03))
* 修复大量问题 ([f911e06](https://github.com/certd/certd/commit/f911e06a698d568ae228880ec89b7e25fdd1305b))
* 修复旧流水线保存时清空证书仓库证书内容的bug ([f58f2f3](https://github.com/certd/certd/commit/f58f2f3cd603c37e55ac5868fdd2ca99aa937e44))
* **pipeline-controller:** 处理非编辑场景下保留id的问题 ([6606f32](https://github.com/certd/certd/commit/6606f3267868e3aadaca0092ef4c83198312553c))
* **pipeline:** 修正流水线保存时的ID判断逻辑并增加参数校验 ([6af12d4](https://github.com/certd/certd/commit/6af12d49fce1144943dc37ba574b3c9356998dcd))
### Performance Improvements
* **dashboard:** 新增仪表盘站点监控和域名管理的统计卡片 ([9842a24](https://github.com/certd/certd/commit/9842a24a50a841f710ee7c8050a38e3334c58394))
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Bug Fixes
@@ -14,6 +14,7 @@ input:
action: onCaTypeList
single: true
mode: tags
emitImmediate: true
required: true
mergeScript: |2-
@@ -42,7 +43,7 @@ input:
return {
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
disabled: ctx.compute(({form})=> !!form.access?.account && !form.access?.editAccount)
}
}
@@ -84,7 +85,7 @@ input:
return form.access?._needEAB === true;
}),
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
disabled: ctx.compute(({form})=> !!form.access?.account && !form.access?.editAccount)
}
}
@@ -102,7 +103,7 @@ input:
return form.access?._needEAB === true;
}),
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
disabled: ctx.compute(({form})=> !!form.access?.account && !form.access?.editAccount)
}
}
@@ -9,7 +9,9 @@ input:
placeholder: api token,用户 API 令牌
helper: |-
前往 [获取API令牌](https://dash.cloudflare.com/profile/api-tokens),注意是令牌,不是密钥。
token权限必须包含:[Zone区域-Zone区域-Edit编辑], [Zone区域-DNS-Edit编辑]
token权限必须包含两条:
[Zone区域-Zone区域-Edit编辑]
[Zone区域-DNS-Edit编辑]
required: true
encrypt: true
proxy:
@@ -0,0 +1,28 @@
name: fastly
title: Fastly授权
icon: simple-icons:fastly
desc: Fastly CDN / TLS Custom Certificates API 授权
input:
apiKey:
title: API Token
component:
placeholder: Fastly API Key / Token
helper: 前往 Fastly Account Settings -> Personal Access Tokens 创建 Token
required: true
encrypt: true
proxy:
title: HTTP代理
component:
placeholder: http://xxxx.xxx.xx:10811
helper: 可选:是否使用 HTTP 代理访问 Fastly API
required: false
encrypt: false
testRequest:
title: 测试
component:
name: api-test
action: TestRequest
helper: 测试授权是否正确
pluginType: access
type: builtIn
scriptFilePath: /plugins/plugin-fastly/access.js
@@ -150,6 +150,15 @@ input:
helper: 请选择distributions id
order: 0
waitForDeployed:
title: 等待部署完成
value: false
helper: >-
开启后,会轮询等待每个CloudFront分配状态变为Deployed(通常需要几分钟)再结束任务。关闭时,提交证书更新后立即结束——CloudFront会在后台自行完成部署。仅当后续任务依赖证书已全球生效时才需要开启。
component:
name: a-switch
vModel: checked
order: 0
output: {}
pluginType: deploy
type: builtIn
@@ -90,7 +90,7 @@ input:
5. <b>自动匹配</b>:此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式
6. <b>DNS持久验证</b>:需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅
Let's Encrypt 测试环境可以申请
Let's Encrypt 测试环境 可以申请
order: 0
dnsProviderType:
title: DNS解析服务商
@@ -180,7 +180,7 @@ input:
}else if (form.challengeType === 'dnses'){
return '给每个域名单独配置dns提供商'
}else if (form.challengeType === 'dns-persist'){
return '请先创建并校验_validation-persist TXT持久记录,校验成功后才能提交流水线;当前仅 Let\'s Encrypt 测试环境可以申请'
return '请先创建并校验_validation-persist TXT持久记录,校验成功后才能提交流水线;当前仅 Let\'s Encrypt 测试环境 可以申请'
}
})
}
@@ -207,7 +207,7 @@ input:
return {
component:{
on: {
"selected-change": (scope)=>{
"selectedChange": (scope)=>{
// 切换颁发机构后,清空已选择的ACME账号,避免账号与颁发机构不匹配
let form = scope.form || {};
form = form.input || form.body || form;
@@ -0,0 +1,95 @@
showRunStrategy: false
default:
strategy:
runStrategy: 1
name: FastlyDeployCert
title: Fastly-部署TLS激活
desc: 部署 Fastly 证书 (创建 TLS Activation 绑定证书到域名)
icon: simple-icons:fastly
group: cdn
input:
cert:
title: 域名证书
helper: 选择前置任务输出的域名证书(将自动上传到 Fastly),或选择前置【Fastly-上传证书到Fastly】任务输出的 Fastly 证书ID。
component:
name: output-selector
from:
- ':cert:'
- FastlyUploadCert
required: true
order: 0
name:
title: 证书名称
helper: 可选。当传入的是域名证书需要先上传时,作为 Fastly 上的自定义证书名称/标签
component:
placeholder: '例如: my-fastly-cert'
required: false
order: 0
accessId:
title: Access授权
helper: Fastly 授权凭证
component:
name: access-selector
type: fastly
required: true
order: 0
domainIds:
title: TLS 域名
component:
name: remote-select
vModel: value
mode: tags
type: plugin
action: onGetTlsDomainList
search: false
pager: false
single: false
watches:
- certDomains
- accessId
required: true
mergeScript: |2-
return {
component:{
form: ctx.compute(({form})=>{
return form
})
},
}
helper: >-
选择要绑定证书的域名,可多选(将为每个域名创建或更新一个 TLS 激活)。列表为你 Fastly 服务上配置的域名,请选择被证书覆盖的具体主机名(如
www.example.com),而非通配 *.example.com(除非确有服务使用通配域名)。列表没有的也可直接输入。
order: 0
configurationId:
title: TLS 配置
component:
name: remote-select
vModel: value
mode: tags
type: plugin
action: onGetTlsConfigurationList
search: false
pager: false
single: true
watches:
- certDomains
- accessId
required: true
mergeScript: |2-
return {
component:{
form: ctx.compute(({form})=>{
return form
})
},
}
helper: 选择关联的 TLS 配置
order: 0
output: {}
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-fastly/plugins/plugin-deploy-to-service.js
@@ -0,0 +1,49 @@
showRunStrategy: false
default:
strategy:
runStrategy: 1
name: FastlyPurgeCache
title: Fastly-清除缓存
desc: 清除 Fastly 指定 Service 的所有缓存 (Purge All)
icon: simple-icons:fastly
group: cdn
input:
accessId:
title: Access授权
helper: Fastly 授权凭证
component:
name: access-selector
type: fastly
required: true
order: 0
serviceId:
title: Service (服务)
component:
name: remote-select
vModel: value
mode: tags
type: plugin
action: onGetServiceList
search: false
pager: false
single: false
watches:
- certDomains
- accessId
required: true
mergeScript: |2-
return {
component:{
form: ctx.compute(({form})=>{
return form
})
},
}
helper: 选择要清理所有缓存的 Fastly 服务
order: 0
output: {}
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-fastly/plugins/plugin-purge-cache.js
@@ -0,0 +1,60 @@
showRunStrategy: false
default:
strategy:
runStrategy: 1
name: FastlyRefreshCert
title: Fastly-更新证书
desc: 自动更新 Fastly CDN 上的已有自定义证书
icon: simple-icons:fastly
group: cdn
input:
cert:
title: 域名证书
helper: 请选择前置任务输出的域名证书
component:
name: output-selector
from:
- ':cert:'
required: true
order: 0
accessId:
title: Access授权
helper: Fastly 授权凭证
component:
name: access-selector
type: fastly
required: true
order: 0
certList:
title: 证书列表
component:
name: remote-select
vModel: value
mode: tags
type: plugin
action: onGetCertList
search: false
pager: false
single: false
watches:
- certDomains
- accessId
required: true
mergeScript: |2-
return {
component:{
form: ctx.compute(({form})=>{
return form
})
},
}
helper: >-
选择要更新的 Fastly 证书 (必须是已存在的自定义证书)。注意:Fastly
要求更新后的证书与原证书使用相同私钥,请确保续期时复用私钥(reuse key),否则更新会失败。
order: 0
output: {}
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-fastly/plugins/plugin-refresh-cert.js
@@ -0,0 +1,49 @@
showRunStrategy: false
default:
strategy:
runStrategy: 1
name: FastlyUploadCert
title: Fastly-上传证书到Fastly
desc: 部署 / 上传 SSL/TLS 自定义证书到 Fastly CDN
icon: simple-icons:fastly
group: cdn
input:
cert:
title: 域名证书
helper: 请选择前置任务输出的域名证书
component:
name: output-selector
from:
- ':cert:'
required: true
order: 0
accessId:
title: Access授权
helper: Fastly 授权凭证
component:
name: access-selector
type: fastly
required: true
order: 0
certificateId:
title: 证书ID (tls_certificate_id)
helper: >-
可选。若填写则对 Fastly 已有证书进行更新(PATCH);留空则新建上传(POST)。注意:Fastly
更新证书要求新证书与原证书使用相同的私钥,请确保续期时复用私钥(reuse key),否则更新会失败。
component:
placeholder: '例如: tls_cert_xxx (留空表示新建)'
required: false
order: 0
name:
title: 证书名称
helper: 可选。上传到 Fastly 上的自定义证书名称/标签
component:
placeholder: '例如: my-fastly-cert'
required: false
order: 0
output:
fastlyCertId:
title: Fastly 证书 ID
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-fastly/plugins/plugin-upload-cert.js
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.44.0",
"version": "1.44.4",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -43,20 +43,20 @@
"lint1": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/acme-client": "^1.44.0",
"@certd/basic": "^1.44.0",
"@certd/commercial-core": "^1.44.0",
"@certd/acme-client": "^1.44.4",
"@certd/basic": "^1.44.4",
"@certd/commercial-core": "^1.44.4",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.44.0",
"@certd/lib-huawei": "^1.44.0",
"@certd/lib-k8s": "^1.44.0",
"@certd/lib-server": "^1.44.0",
"@certd/midway-flyway-js": "^1.44.0",
"@certd/pipeline": "^1.44.0",
"@certd/plugin-cert": "^1.44.0",
"@certd/plugin-lib": "^1.44.0",
"@certd/plugin-plus": "^1.44.0",
"@certd/plus-core": "^1.44.0",
"@certd/jdcloud": "^1.44.4",
"@certd/lib-huawei": "^1.44.4",
"@certd/lib-k8s": "^1.44.4",
"@certd/lib-server": "^1.44.4",
"@certd/midway-flyway-js": "^1.44.4",
"@certd/pipeline": "^1.44.4",
"@certd/plugin-cert": "^1.44.4",
"@certd/plugin-lib": "^1.44.4",
"@certd/plugin-plus": "^1.44.4",
"@certd/plus-core": "^1.44.4",
"@koa/cors": "^5.0.0",
"@midwayjs/bootstrap": "3.20.11",
"@midwayjs/cache": "3.14.0",
@@ -45,6 +45,7 @@ process.on("uncaughtException", error => {
// setInterval(log, 200);
// log()
// }
// startHeapLog();
@Configuration({
@@ -128,7 +129,6 @@ export class MainConfiguration {
ctx.response.set("Cache-Control", "public,max-age=0");
}
});
//acme setlogger
setLogger((text: string) => {
logger.info(text);
@@ -3,6 +3,7 @@ import { merge } from "lodash-es";
import { CrudController } from "@certd/lib-server";
import {
OnlinePluginAuthorAddReq,
OnlinePluginAuthorUpdateReq,
OnlinePluginDependenciesReq,
OnlinePluginInstallReq,
OnlinePluginListReq,
@@ -103,11 +104,11 @@ export class PluginController extends CrudController<PluginService> {
await this.auditLog({ content: "保存了公共插件配置" });
return this.ok(res);
}
@Post("/getPluginByName", { description: "sys:settings:view" })
async getPluginByName(@Body("name") name: string) {
@Post("/getSetting", { description: "sys:settings:view" })
async getSetting(@Body("name") name: string, @Body("type") type: string) {
const res = await this.pluginConfigService.getPluginConfig({
name: name,
type: "builtIn",
name,
type,
});
return this.ok(res);
}
@@ -192,6 +193,11 @@ export class PluginController extends CrudController<PluginService> {
return this.ok(res);
}
@Post("/online/author/update", { description: "sys:settings:edit", summary: "修改在线插件作者邮箱" })
async onlineAuthorUpdate(@Body(ALL) body: OnlinePluginAuthorUpdateReq) {
return this.ok(await this.service.updateOnlinePluginAuthor(body));
}
@Post("/online/publish/info", { description: "sys:settings:view", summary: "查询本地插件发布信息" })
async onlinePublishInfo(@Body(ALL) body: OnlinePluginPublishInfoReq) {
const res = await this.service.getOnlinePluginPublishInfo(body);
@@ -203,4 +209,10 @@ export class PluginController extends CrudController<PluginService> {
const res = await this.service.exportPlugin(id);
return this.ok(res);
}
@Post("/getPluginDefine", { description: "sys:settings:view", summary: "根据插件名称获取插件定义" })
async getPluginDefine(@Body("name") name: string) {
const res = await this.service.getPluginDefine(name);
return this.ok(res);
}
}
@@ -5,7 +5,12 @@ import { RoleService } from "../../../modules/sys/authority/service/role-service
import { PipelineService } from "../../../modules/pipeline/service/pipeline-service.js";
import { HistoryService } from "../../../modules/pipeline/service/history-service.js";
import { CertInfoService, CertStatus } from "../../../modules/monitor/index.js";
import { SiteInfoService } from "../../../modules/monitor/service/site-info-service.js";
import { DomainService } from "../../../modules/cert/service/domain-service.js";
import { Between, LessThan } from "typeorm";
import { ApiTags } from "@midwayjs/swagger";
import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js";
import { UserStatisticSetting } from "../../../modules/mine/service/models.js";
export type ChartItem = {
name: string;
@@ -25,7 +30,24 @@ export type UserStatisticCount = {
expiring: number;
notExpired: number;
};
siteCount?: {
total: number;
abnormal: number;
normal: number;
};
domainCount?: {
total: number;
expired: number;
expiring: number;
notExpired: number;
};
expiringList: any[];
genCertCount: {
singleDomainCertCount: number;
multiDomainCertCount: number;
wildcardCertCount: number;
totalPipelineRuns: number;
};
};
/**
*/
@@ -46,6 +68,15 @@ export class StatisticController extends BaseController {
@Inject()
certInfoService: CertInfoService;
@Inject()
siteInfoService: SiteInfoService;
@Inject()
domainService: DomainService;
@Inject()
userSettingsService: UserSettingsService;
@Post("/count", { description: Constants.per.authOnly, summary: "查询仪表盘统计数据" })
public async count() {
const { userId, projectId } = await this.getProjectUserIdRead();
@@ -57,14 +88,33 @@ export class StatisticController extends BaseController {
const expiringList = await this.pipelineService.latestExpiringList({ userId, projectId, count: 5 });
const certCount = await this.certInfoService.count({ userId, projectId, status: CertStatus.active });
const now = Date.now();
const oneMonthLater = now + 30 * 24 * 60 * 60 * 1000;
const userProjectQuery = this.siteInfoService.buildUserProjectQuery(userId, projectId);
const siteTotal = await this.siteInfoService.repository.count({ where: userProjectQuery });
const siteAbnormal = await this.siteInfoService.repository.count({
where: { ...userProjectQuery, checkStatus: "error" },
});
const domainProjectQuery = this.domainService.buildUserProjectQuery(userId, projectId);
const domainTotal = await this.domainService.repository.count({ where: domainProjectQuery });
const domainExpired = await this.domainService.repository.count({
where: { ...domainProjectQuery, expirationDate: LessThan(now) },
});
const domainExpiring = await this.domainService.repository.count({
where: { ...domainProjectQuery, expirationDate: Between(now, oneMonthLater) },
});
const userStatistic = await this.userSettingsService.getSetting<UserStatisticSetting>(userId, projectId, UserStatisticSetting);
const count: UserStatisticCount = {
pipelineCount,
pipelineStatusCount,
pipelineEnableCount,
certCount,
siteCount: { total: siteTotal, abnormal: siteAbnormal, normal: siteTotal - siteAbnormal },
domainCount: { total: domainTotal, expired: domainExpired, expiring: domainExpiring, notExpired: domainTotal - domainExpired - domainExpiring },
historyCountPerDay: historyCount,
expiringList,
genCertCount: userStatistic.genCertCount,
};
return this.ok(count);
}
@@ -201,12 +201,16 @@ export class PipelineController extends CrudController<PipelineService> {
@Post("/save", { description: Constants.per.authOnly, summary: "新增/更新流水线" })
async save(@Body() bean: PipelineSaveDTO) {
const { userId, projectId } = await this.getProjectUserIdWrite();
const isNew = bean.id <= 0;
if (bean.id < 0) {
throw new Error("流水线ID不能小于0");
}
const isNew = bean.id == 0 || bean.id == null;
if (!isNew) {
const { userId, projectId } = await this.checkOwner(this.getService(), bean.id, "write", true);
bean.userId = userId;
bean.projectId = projectId;
} else {
delete bean.id;
bean.userId = userId;
bean.projectId = projectId;
}
@@ -6,6 +6,7 @@ import { getRuntimeDepsService, initRuntimeDepsService } from "@certd/pipeline";
import { pluginRegistry, accessRegistry, notificationRegistry } from "@certd/pipeline";
import { dnsProviderRegistry } from "@certd/plugin-lib";
import { addonRegistry } from "@certd/lib-server";
import { PluginConfigService } from "../plugin/service/plugin-config-service.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -13,6 +14,9 @@ export class AutoLoadPlugins {
@Inject()
pluginService: PluginService;
@Inject()
pluginConfigService: PluginConfigService;
@Config("runtimeDeps")
runtimeDepsConfig: any;
@@ -34,8 +38,8 @@ export class AutoLoadPlugins {
}
// await import("../../plugins/index.js")
await this.pluginService.registerFromDb();
await registerPaymentProviders();
await this.pluginConfigService.loadAllPluginSetting();
registerPaymentProviders();
logger.info(`加载插件完成,加载模式:${process.env.certd_plugin_loadmode}`);
//初始化第三方依赖服务
@@ -93,6 +93,7 @@ export class CommonEabToAcmeAccountFix {
...certApplyConfig.sysSetting,
input,
},
type: "builtIn",
});
}
return true;
@@ -108,11 +108,8 @@ describe("DnsPersistRecordService", () => {
param.id = 77;
saved = { ...param };
},
async findOneBy(where: any) {
return where.id === 77 ? saved : null;
},
async findOne() {
return null;
return saved;
},
};
(service as any).accessService = {
@@ -218,11 +215,11 @@ describe("DnsPersistRecordService", () => {
saved = { ...saved, ...param };
},
async findOne(options: any) {
if (options.where.id === 89) {
return saved;
}
return options.where.domain === "example.com" && options.where.acmeAccountAccessId === 1 ? saved : null;
},
async findOneBy(where: any) {
return where.id === 89 ? saved : null;
},
};
(service as any).accessService = {
async getAccessById() {
@@ -256,8 +253,8 @@ describe("DnsPersistRecordService", () => {
const service = new DnsPersistRecordService();
let deletedIds: any = null;
(service as any).repository = {
async findOneBy(where: any) {
return where.id === 90
async findOne(options: any) {
return options.where.id === 90
? {
id: 90,
domain: "example.com",
@@ -292,8 +289,8 @@ describe("DnsPersistRecordService", () => {
status: "pending",
};
(service as any).repository = {
async findOneBy(where: any) {
return where.id === 91 ? saved : null;
async findOne(options: any) {
return options.where.id === 91 ? saved : null;
},
async save(param: any) {
saved = { ...saved, ...param };
@@ -73,10 +73,11 @@ export class DomainService extends BaseService<DomainEntity> {
if (!param.domain) {
throw new Error("domain 不能为空");
}
const old = await this.repository.findOne({
const query = this.buildUserProjectQuery(param.userId, param.projectId);
const old = await this.findOne({
where: {
domain: param.domain,
userId: param.userId,
...query,
},
});
if (old) {
@@ -96,11 +97,11 @@ export class DomainService extends BaseService<DomainEntity> {
if (!old) {
throw new Error("domain记录不存在");
}
const query = this.buildUserProjectQuery(param.userId, param.projectId);
const same = await this.repository.findOne({
where: {
domain: param.domain,
userId: old.userId,
...query,
id: Not(param.id),
},
});
@@ -222,7 +223,6 @@ export class DomainService extends BaseService<DomainEntity> {
}
domainVerifiers[domain] = null;
}
return domainVerifiers;
}
@@ -52,6 +52,20 @@ export class UserGrantSetting extends BaseSettings {
allowAdminViewCerts = false;
}
export class UserStatisticSetting extends BaseSettings {
static __title__ = "用户累计统计";
static __key__ = "user.statistic";
genCertCount = {
singleDomainCertCount: 0,
multiDomainCertCount: 0,
wildcardCertCount: 0,
totalPipelineRuns: 0,
};
}
export type UserStatisticField = `genCertCount.${keyof UserStatisticSetting["genCertCount"]}`;
export class UserPreferencesSetting extends BaseSettings {
static __title__ = "用户偏好设置";
static __key__ = "user.preferences";
@@ -3,7 +3,9 @@ import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { BaseService, BaseSettings } from "@certd/lib-server";
import { UserSettingsEntity } from "../entity/user-settings.js";
import { LocalCache, mergeUtils } from "@certd/basic";
import { LocalCache, locker, mergeUtils } from "@certd/basic";
import { UserStatisticField, UserStatisticSetting } from "./models.js";
import { get, set } from "lodash-es";
const { merge } = mergeUtils;
const UserSettingCache = new LocalCache({
@@ -133,4 +135,14 @@ export class UserSettingsService extends BaseService<UserSettingsEntity> {
newEntity.setting = JSON.stringify(bean);
await this.repository.save(newEntity);
}
async incrementStatistic(userId: number, projectId: number | null | undefined, field: UserStatisticField) {
const lockKey = `user-statistic:${userId}:${projectId ?? "global"}`;
await locker.execute(lockKey, async () => {
const setting = await this.getSetting<UserStatisticSetting>(userId, projectId ?? undefined, UserStatisticSetting);
const current = Number(get(setting, field, 0)) || 0;
set(setting, field, current + 1);
await this.saveSetting(userId, projectId ?? undefined, setting);
});
}
}
@@ -23,6 +23,14 @@ function createSelfSignedCert(domain = "example.com") {
};
}
function createCertInfoService() {
const service = new CertInfoService();
service.userSettingsService = {
async incrementStatistic() {},
} as any;
return service;
}
describe("CertInfoService", () => {
it("counts wildcard domains by normalized prefix", () => {
const service = new CertInfoService();
@@ -32,7 +40,7 @@ describe("CertInfoService", () => {
describe("updateCertByPipelineId 多条模式", () => {
it("新建激活证书并绑定流水线id、继承已有记录来源", async () => {
const service = new CertInfoService();
const service = createCertInfoService();
// 该流水线已有旧证书记录(upload 来源),但没有空证书记录
const existing = { id: 10, userId: 2, projectId: 3, pipelineId: 1, fromType: "upload", status: CertStatus.inactive };
service.repository = {
@@ -74,8 +82,11 @@ describe("CertInfoService", () => {
});
it("无已有记录时按流水线类型推导来源,并把同流水线旧激活证书标记为未激活", async () => {
const service = new CertInfoService();
const service = createCertInfoService();
service.repository = {
async find() {
return [];
},
async findOne() {
return null;
},
@@ -117,8 +128,11 @@ describe("CertInfoService", () => {
});
it("无已有记录且流水线类型无对应来源时,使用传入来源", async () => {
const service = new CertInfoService();
const service = createCertInfoService();
service.repository = {
async find() {
return [];
},
async findOne() {
return null;
},
@@ -143,8 +157,11 @@ describe("CertInfoService", () => {
});
it("传申请任务id时,新证书记录任务id,且只把同任务旧证书标记为未激活", async () => {
const service = new CertInfoService();
const service = createCertInfoService();
service.repository = {
async find() {
return [];
},
async findOne() {
return null;
},
@@ -182,10 +199,13 @@ describe("CertInfoService", () => {
});
it("存在空证书记录时,申请成功后直接更新占位记录,不新建记录", async () => {
const service = new CertInfoService();
const service = createCertInfoService();
// 保存流水线时创建的空证书记录(占位,certInfo 为空,已记录申请任务id)
const emptyRecord = { id: 184, userId: 2, projectId: 1, pipelineId: 209, fromType: "pipeline", status: CertStatus.active, taskId: "PgaUhdeO1DWB_BLbIlzqV", certInfo: null };
service.repository = {
async find() {
return [];
},
async findOne() {
// 既有记录查询与空证书记录查询都命中同一条占位记录
return emptyRecord;
@@ -228,7 +248,7 @@ describe("CertInfoService", () => {
});
it("updateCertByPipelineId 从流水线配置解析并记录 ACME账号授权id(供吊销读表,不再查流水线)", async () => {
const service = new CertInfoService();
const service = createCertInfoService();
service.repository = {
async findOne(args: any) {
const where = args?.where ?? args;
@@ -282,6 +302,9 @@ describe("CertInfoService", () => {
it("updateDomains 保存流水线时为申请任务创建空证书记录(记录任务id)", async () => {
const service = new CertInfoService();
service.repository = {
async find() {
return [];
},
async findOne() {
return null;
},
@@ -312,6 +335,9 @@ describe("CertInfoService", () => {
const service = new CertInfoService();
const existing = { id: 10, pipelineId: 1, userId: 2, projectId: 3, fromType: "auto", taskId: "apply-step-1", status: CertStatus.active };
service.repository = {
async find() {
return [existing];
},
async findOne() {
return existing;
},
@@ -337,6 +363,9 @@ describe("CertInfoService", () => {
const service = new CertInfoService();
let findOneCount = 0;
service.repository = {
async find() {
return [];
},
async findOne() {
findOneCount++;
return null;
@@ -374,6 +403,9 @@ describe("CertInfoService", () => {
it("updateDomains 删除孤儿 active 记录(流水线中已不存在的任务,含 taskId 为空的遗留记录)", async () => {
const service = new CertInfoService();
service.repository = {
async find() {
return [];
},
async findOne() {
return null;
},
@@ -7,6 +7,7 @@ import { logger, utils } from "@certd/basic";
import { CertInfo, CertReader } from "@certd/plugin-cert";
import { AcmeService } from "../../../plugins/plugin-cert/plugin/cert-plugin/acme.js";
import { PipelineEntity } from "../../pipeline/entity/pipeline.js";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
export type UploadCertReq = {
id?: number;
@@ -39,6 +40,9 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
@InjectEntityModel(PipelineEntity)
pipelineRepository: Repository<PipelineEntity>;
@Inject()
userSettingsService: UserSettingsService;
//@ts-ignore
getRepository() {
return this.repository;
@@ -213,6 +217,12 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
status: CertStatus.inactive,
}
);
if (certUserId != null) {
const domains = cert?.crt ? new CertReader(cert).getAltNames() : [];
const wildcardCount = domains.filter(domain => String(domain).trim().toLowerCase().startsWith("*.")).length;
const field = wildcardCount > 0 ? "genCertCount.wildcardCertCount" : domains.length > 1 ? "genCertCount.multiDomainCertCount" : "genCertCount.singleDomainCertCount";
await this.userSettingsService.incrementStatistic(certUserId, certProjectId, field);
}
return bean;
}
@@ -243,20 +253,28 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
}
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
const taskIds = applyTasks.map(task => task.taskId);
const foundHistory = await this.repository.find({
where: {
pipelineId,
status: CertStatus.active,
...userProjectQuery,
},
});
for (const task of applyTasks) {
// 每个申请任务维护一条 active 记录(不重复创建,最新真证书申请成功后旧记录会被标记为 inactive)
const found = await this.repository.findOne({
where: {
pipelineId,
taskId: task.taskId,
status: CertStatus.active,
...userProjectQuery,
},
});
const bean = new CertInfoEntity();
let found = foundHistory.find(item => item.taskId === task.taskId);
if (!found && foundHistory.length == 1 && applyTasks.length == 1) {
found = foundHistory[0];
}
if (found) {
//已有 active 记录(空占位或真证书):更新域名信息
bean.id = found.id;
if (!found.taskId) {
bean.taskId = task.taskId;
}
} else {
//创建空证书记录
bean.pipelineId = pipelineId;
@@ -281,7 +299,6 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
taskId: Or(IsNull(), Not(In(taskIds))),
});
}
/**
* 根据流水线类型推导证书来源(与流水线保存逻辑保持一致)
*/

Some files were not shown because too many files have changed in this diff Show More