Compare commits

..

98 Commits

Author SHA1 Message Date
xiaojunnuo
7bdde68ece perf: 登录注册、找回密码都支持极验验证码和图片验证码 2025-09-13 23:01:14 +08:00
xiaojunnuo
50f92f55e2 chore: 2025-09-13 16:27:20 +08:00
xiaojunnuo
370db62bf0 perf: 登录支持极验验证码 2025-09-11 23:47:05 +08:00
xiaojunnuo
65f34f1d31 Merge branch 'v2-dev' into v2-dev-addon 2025-09-11 20:42:44 +08:00
xiaojunnuo
00a3908abb docs: 2025-09-11 15:20:13 +08:00
xiaojunnuo
32034d590a docs: 2025-09-11 11:24:51 +08:00
xiaojunnuo
3635fb3910 chore: 2025-09-11 00:19:38 +08:00
xiaojunnuo
d2ecfe5491 fix: 修复证书监控某些情况下报 options.lookup不能为null的bug 2025-09-10 14:12:36 +08:00
xiaojunnuo
1f759dce5b docs: 2025-09-10 12:21:04 +08:00
xiaojunnuo
ae41c6038b perf: ssh配置增加脚本类型设置,bash还是sh 2025-09-09 18:14:14 +08:00
xiaojunnuo
f41f7eb2ad Merge remote-tracking branch 'origin/v2-dev' into v2-dev 2025-09-09 16:31:39 +08:00
xiaojunnuo
d04f383161 fix: 修复secret patch 类型多了type:的bug 2025-09-09 16:30:21 +08:00
xiaojunnuo
cb989d7489 Merge remote-tracking branch 'origin/v2-dev' into v2-dev 2025-09-08 23:04:25 +08:00
xiaojunnuo
b5cba19d26 chore: 2025-09-08 23:04:02 +08:00
xiaojunnuo
b7271d7a46 perf: start.sh增加sudo 2025-09-08 23:01:45 +08:00
xiaojunnuo
521083a309 chore: 2025-09-08 14:45:31 +08:00
xiaojunnuo
6d35325601 Merge remote-tracking branch 'origin/v2-dev' into v2-dev 2025-09-08 14:45:21 +08:00
xiaojunnuo
3c65f37d84 perf: 优化加量包展示效果 2025-09-08 14:43:36 +08:00
xiaojunnuo
d75dd058d6 fix: 修复商业版退出登录后,丢失站点个性化设置的bug 2025-09-08 14:29:15 +08:00
xiaojunnuo
40475e02ec chore: 2025-09-06 20:07:50 +08:00
COYG⚡️
f6ea9c1300 docs: 更改中英文档跳转链接显示形式 (#518) @1411430556
* Update README.md

* Update README_en.md
2025-09-06 00:43:08 +08:00
Zero Clover
902359f24e perf: add preferred chain option (#519) @ZeroClover 2025-09-06 00:41:03 +08:00
xiaojunnuo
bb4d5f1e93 build: publish 2025-09-06 00:35:14 +08:00
xiaojunnuo
1dec3f000e build: trigger build image 2025-09-06 00:34:58 +08:00
xiaojunnuo
6d89814795 v1.36.19 2025-09-06 00:33:10 +08:00
xiaojunnuo
f339bc9f7f build: prepare to build 2025-09-06 00:30:00 +08:00
xiaojunnuo
bb80bc0c07 chore: 2025-09-06 00:29:55 +08:00
xiaojunnuo
96677ff8bf build: prepare to build 2025-09-06 00:28:54 +08:00
xiaojunnuo
c7b6a6df79 chore: 2025-09-06 00:28:50 +08:00
xiaojunnuo
8bb7e8bfb2 chore: 2025-09-06 00:28:43 +08:00
xiaojunnuo
02ab343e22 build: prepare to build 2025-09-06 00:26:48 +08:00
xiaojunnuo
4d875a18de chore: 2025-09-06 00:26:29 +08:00
xiaojunnuo
cff2336923 build: prepare to build 2025-09-06 00:17:10 +08:00
xiaojunnuo
0e96bfdfa3 perf: 创建证书时支持选择通知时机 2025-09-06 00:12:16 +08:00
xiaojunnuo
a24ef48ad1 chore: 2025-09-06 00:01:45 +08:00
xiaojunnuo
fe9c4f3391 perf: 支持根据id更新证书(证书Id不变接口),不过该接口为白名单功能,普通腾讯云账户无法使用 2025-09-06 00:01:17 +08:00
xiaojunnuo
6cbb0739f8 fix: 修复远程数据选择无法过滤的bug 2025-09-05 22:19:03 +08:00
xiaojunnuo
79ebabfcfb perf: 创建k8s secret 时设置type为tls 2025-09-05 21:32:34 +08:00
xiaojunnuo
0c8e3262fe chore: 2025-09-05 21:17:15 +08:00
xiaojunnuo
c24a040c19 perf: ssh 增加超时断开连接,默认10分钟超时 2025-09-05 21:16:09 +08:00
xiaojunnuo
4f39cb8dfa chore: 2025-09-05 18:08:23 +08:00
xiaojunnuo
cdd2816642 chore: 2025-09-05 00:16:34 +08:00
xiaojunnuo
27b6dfa4d2 perf: 支持ssl.com证书颁发机构 2025-09-04 23:42:03 +08:00
xiaojunnuo
204cbd0209 chore: 2025-09-04 15:21:53 +08:00
xiaojunnuo
b7980aad5a perf: 支持godaddy 2025-09-04 15:13:45 +08:00
xiaojunnuo
e175729e2c chore: 2025-09-02 10:39:46 +08:00
xiaojunnuo
c26ad4c807 fix: 修复mysql下购买套餐加量包无效的bug 2025-09-02 10:37:36 +08:00
xiaojunnuo
4372adc703 fix: 修复批量流水线执行时日志显示错乱的问题 2025-09-01 18:10:32 +08:00
xiaojunnuo
8a0c2b9b13 perf: 去掉宝塔url后面的斜杠 2025-09-01 17:01:14 +08:00
xiaojunnuo
4443a1c030 perf: 商业版隐藏文档相关链接 2025-09-01 16:18:50 +08:00
xiaojunnuo
39a02235cf perf: 子域名托管说明 2025-09-01 15:52:19 +08:00
xiaojunnuo
db89561480 perf: 商业版隐藏文档相关链接 2025-09-01 15:52:14 +08:00
xiaojunnuo
a4cbb11693 chore: 2025-09-01 14:18:42 +08:00
xiaojunnuo
1ceeacc526 chore: 2025-09-01 13:33:12 +08:00
xiaojunnuo
b59052cc43 fix: 前置任务输出不存在时输出警告提示 2025-09-01 13:29:47 +08:00
xiaojunnuo
44019e1042 perf: 增加健康检查探针 /health/liveliness 和 /health/readiness 2025-08-29 10:07:17 +08:00
xiaojunnuo
fd0e1da4a2 build: publish 2025-08-29 00:43:39 +08:00
xiaojunnuo
f6c67b475a build: trigger build image 2025-08-29 00:43:24 +08:00
xiaojunnuo
ea18a5ad15 v1.36.18 2025-08-29 00:41:56 +08:00
xiaojunnuo
4d0cd3f497 build: prepare to build 2025-08-29 00:39:03 +08:00
xiaojunnuo
7dbdeaebe0 perf: 支持部署到dokploy 2025-08-29 00:38:45 +08:00
xiaojunnuo
2085bcceb6 perf: openapi返回证书时挑选匹配范围最小的那一个;增加format参数,增加返回值p7b格式,增加detail返回 2025-08-28 22:39:11 +08:00
xiaojunnuo
c09c962cb6 perf: 部署到k8s支持自动创建secret 2025-08-28 21:28:32 +08:00
xiaojunnuo
9108459ae4 perf: 短信验证码支持腾讯云 2025-08-28 17:35:17 +08:00
xiaojunnuo
992bac0b1f chore: 2025-08-28 15:53:15 +08:00
xiaojunnuo
ebd6917a1d fix: 修复proxmox某些情况下执行卡住的bug 2025-08-28 15:47:32 +08:00
xiaojunnuo
3e079e3b80 chore: 2025-08-28 00:58:17 +08:00
xiaojunnuo
2ca20be197 perf: 支持部署到阿里云云原生API网关、AI网关 2025-08-28 00:36:28 +08:00
xiaojunnuo
17f23f3751 perf: 商业版支持自定义插件的参数配置 2025-08-27 18:23:24 +08:00
xiaojunnuo
8e3d699856 chore: 2025-08-27 09:56:36 +08:00
xiaojunnuo
f1a168fa53 chore: plugin config 2025-08-26 18:42:54 +08:00
xiaojunnuo
3575113655 perf: 支持删除宝塔证书夹中的过期证书 2025-08-25 23:58:03 +08:00
xiaojunnuo
9feb9d04b3 perf: 支持部署到华为云obs 2025-08-25 23:22:17 +08:00
xiaojunnuo
5419b1439a Merge branch 'v2' into v2-dev 2025-08-25 21:52:50 +08:00
xiaojunnuo
e4489343fe perf: lecdnv2支持api token 2025-08-25 18:38:35 +08:00
xiaojunnuo
d9f4a5793d perf: 支持p7b证书格式 2025-08-25 18:21:38 +08:00
xiaojunnuo
70fcdc9ebb perf: 腾讯云EO插件支持自动获取zoneid和域名列表 2025-08-25 17:22:55 +08:00
SHAREWEBS
78e7a81638 fix: 更新我爱云CDN域名地址,和部分目录结构 @tyjsjxh (#514)
更新我爱云CDN域名地址,和部分目录结构
2025-08-25 16:30:23 +08:00
xiaojunnuo
58e82d5dbd perf: 腾讯云插件支持国际版 2025-08-25 16:19:37 +08:00
xiaojunnuo
06d15be43a chore: 2025-08-21 15:57:39 +08:00
xiaojunnuo
e1e7011853 perf: ssh 配置sudo免密提示 2025-08-19 17:06:14 +08:00
xiaojunnuo
eff7645035 chore: 移除ksyun-sdk-node包 2025-08-19 11:01:01 +08:00
xiaojunnuo
eb75e52278 fix: 修复cron选择组件星期显示错误的bug 2025-08-18 18:48:42 +08:00
xiaojunnuo
15e6148272 chore: 2025-08-18 00:28:55 +08:00
xiaojunnuo
ccd448a675 chore: 2025-08-18 00:27:31 +08:00
xiaojunnuo
db54c019ad chore: 2025-08-18 00:16:28 +08:00
xiaojunnuo
b762b4d72c build: publish 2025-08-17 23:58:37 +08:00
xiaojunnuo
2f8faa839d build: trigger build image 2025-08-17 23:58:22 +08:00
xiaojunnuo
831c325c63 v1.36.17 2025-08-17 23:56:48 +08:00
xiaojunnuo
f4f73078c5 build: prepare to build 2025-08-17 23:53:21 +08:00
xiaojunnuo
f7d43ad5af perf: 部署到腾讯云cdn,每个域名增加3每秒延迟 2025-08-17 23:43:39 +08:00
xiaojunnuo
a77c777980 perf: 腾讯云关闭证书通知增加开关选项,在腾讯云授权里面 2025-08-17 23:32:29 +08:00
xiaojunnuo
a34db7449e perf: 阿里云 FC3.0 不在要求证书加密方式为旧版, 修复支持的协议类型可以正常选择 2025-08-17 23:27:50 +08:00
xiaojunnuo
0283bd2f97 perf: 证书申请任务默认不发送申请成功通知 2025-08-17 23:08:50 +08:00
xiaojunnuo
a8de2f8ae7 Merge remote-tracking branch 'origin/v2-dev' into v2-dev 2025-08-17 20:13:42 +08:00
xiaojunnuo
d5dee75df3 fix: 修复新部署的无法保存公共eab配置的bug 2025-08-17 19:08:08 +08:00
xiaojunnuo
79cb5c0631 build: publish 2025-08-16 12:51:23 +08:00
xiaojunnuo
7d9901540f build: trigger build image 2025-08-16 12:51:04 +08:00
207 changed files with 5810 additions and 869 deletions

View File

@@ -1,6 +1,6 @@
--- ---
name: Plugin Apply name: Plugin Apply
about: 请求支持新部署插件 about: 部署插件申请支持
title: "[Plugin] " title: "[Plugin] "
labels: feature labels: feature
--- ---

View File

@@ -1,6 +1,6 @@
--- ---
name: DNS Provider Apply name: DNS Provider Apply
about: 请求支持新的域名提供商 about: 域名提供商申请支持
title: "[DNS] " title: "[DNS] "
labels: feature labels: feature
--- ---

View File

@@ -1,6 +1,6 @@
--- ---
name: Bug Report name: Bug Report
about: 报告一个错误或问题 about: 错误或问题报告
title: "[BUG] " title: "[BUG] "
labels: bug labels: bug
--- ---

View File

@@ -1,6 +1,6 @@
--- ---
name: Feature Request name: Feature Request
about: 新需求、新特性 about: 新需求、新特性申请支持
title: "[Feature] " title: "[Feature] "
labels: feature labels: feature
--- ---
@@ -9,7 +9,8 @@ labels: feature
> 如果有条件,请尽量在[github上提交](https://github.com/certd/certd/issues) > 如果有条件,请尽量在[github上提交](https://github.com/certd/certd/issues)
# 新需求申请 # 新特性申请
>注意:这里仅供如果是要申请新的部署插件,请提交插件申请
## 1. 需求描述,需求背景 ## 1. 需求描述,需求背景
`请在此处简要描述你所遇到的问题,必要时请贴出相关截图辅助理解` `请在此处简要描述你所遇到的问题,必要时请贴出相关截图辅助理解`

View File

@@ -3,6 +3,66 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Bug Fixes
* 前置任务输出不存在时输出警告提示 ([b59052c](https://github.com/certd/certd/commit/b59052cc43b7b070fabd8b8e914e4c2a5e0ad61c))
* 修复批量流水线执行时日志显示错乱的问题 ([4372adc](https://github.com/certd/certd/commit/4372adc703b9a4c785664054ab2a533626d815a8))
* 修复远程数据选择无法过滤的bug ([6cbb073](https://github.com/certd/certd/commit/6cbb0739f8428d51b0712f718fe4d236cc087cf9))
* 修复mysql下购买套餐加量包无效的bug ([c26ad4c](https://github.com/certd/certd/commit/c26ad4c8075f0606d45b8da13915737968d6191a))
### Performance Improvements
* 创建证书时支持选择通知时机 ([0e96bfd](https://github.com/certd/certd/commit/0e96bfdfa377824d204e72923d1176408ae6b300))
* 创建k8s secret 时设置type为tls ([79ebabf](https://github.com/certd/certd/commit/79ebabfcfb9e5a534049c84f5f1a642b357fc856))
* 去掉宝塔url后面的斜杠 ([8a0c2b9](https://github.com/certd/certd/commit/8a0c2b9b13628da750c25757e0cb8ed3038775ba))
* 商业版隐藏文档相关链接 ([4443a1c](https://github.com/certd/certd/commit/4443a1c0308fa6b95a05efd73d15d24b65d641c9))
* 商业版隐藏文档相关链接 ([db89561](https://github.com/certd/certd/commit/db8956148083bc4f988226ccf719940d08158a27))
* 增加健康检查探针 /health/liveliness 和 /health/readiness ([44019e1](https://github.com/certd/certd/commit/44019e104289fedd32a867db00e9c6cb71b389cc))
* 支持根据id更新证书证书Id不变接口不过该接口为白名单功能普通腾讯云账户无法使用 ([fe9c4f3](https://github.com/certd/certd/commit/fe9c4f3391ff07c01dd9a252225f69a129c39050))
* 支持godaddy ([b7980aa](https://github.com/certd/certd/commit/b7980aad5ab50f58662eaddf5d84aa82876a98eb))
* 支持ssl.com证书颁发机构 ([27b6dfa](https://github.com/certd/certd/commit/27b6dfa4d2ab3bddd284c3a34511a72e1a513a4c))
* 子域名托管说明 ([39a0223](https://github.com/certd/certd/commit/39a02235cf4416bb5bd1acd3831241efeaa2f602))
* ssh 增加超时断开连接默认10分钟超时 ([c24a040](https://github.com/certd/certd/commit/c24a040c19cacafc79228d7a7649af93837d94a1))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Bug Fixes
* 更新我爱云CDN域名地址和部分目录结构 [@tyjsjxh](https://github.com/tyjsjxh) ([#514](https://github.com/certd/certd/issues/514)) ([78e7a81](https://github.com/certd/certd/commit/78e7a81638c2ee779f0ab6c3ba7e5c6f6e064151))
* 修复cron选择组件星期显示错误的bug ([eb75e52](https://github.com/certd/certd/commit/eb75e52278f94a72643f7317e6740fb42666c68a))
* 修复proxmox某些情况下执行卡住的bug ([ebd6917](https://github.com/certd/certd/commit/ebd6917a1d40ae4d94555c32b7e3c093d0599b94))
### Performance Improvements
* 部署到k8s支持自动创建secret ([c09c962](https://github.com/certd/certd/commit/c09c962cb676ca261610aa9f3e5105c9dae43f43))
* 短信验证码支持腾讯云 ([9108459](https://github.com/certd/certd/commit/9108459ae42bcd95a59acba164a64e82e5f2cfe6))
* 商业版支持自定义插件的参数配置 ([17f23f3](https://github.com/certd/certd/commit/17f23f37516af925d5049291d67d41e4271f81f8))
* 腾讯云插件支持国际版 ([58e82d5](https://github.com/certd/certd/commit/58e82d5dbd4ebf089ef239578ef9b68454d17b30))
* 腾讯云EO插件支持自动获取zoneid和域名列表 ([70fcdc9](https://github.com/certd/certd/commit/70fcdc9ebbfb7c883c0c8a2138f61a0776a9491b))
* 支持部署到阿里云云原生API网关、AI网关 ([2ca20be](https://github.com/certd/certd/commit/2ca20be197720201fceabcce9d927f4dbc1cc872))
* 支持部署到华为云obs ([9feb9d0](https://github.com/certd/certd/commit/9feb9d04b3c56ec95c06fcf4fd071eb0e88ffc6f))
* 支持部署到dokploy ([7dbdeae](https://github.com/certd/certd/commit/7dbdeaebe0bfee7521a863fe5e6b4a712aec5876))
* 支持删除宝塔证书夹中的过期证书 ([3575113](https://github.com/certd/certd/commit/3575113655be751d19f88c64491e98a89042d6a2))
* 支持p7b证书格式 ([d9f4a57](https://github.com/certd/certd/commit/d9f4a5793d68a017a5d80ad5385cbda603c4e165))
* lecdnv2支持api token ([e448934](https://github.com/certd/certd/commit/e4489343fee7754be07bcfc3323969dc3a30e90c))
* openapi返回证书时挑选匹配范围最小的那一个增加format参数增加返回值p7b格式增加detail返回 ([2085bcc](https://github.com/certd/certd/commit/2085bcceb61c3723c9bdfec4c4cc0917631ff5e5))
* ssh 配置sudo免密提示 ([e1e7011](https://github.com/certd/certd/commit/e1e7011853ad0c5bd7b09c3690861d5aa34b2db4))
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
### Bug Fixes
* 修复新部署的无法保存公共eab配置的bug ([d5dee75](https://github.com/certd/certd/commit/d5dee75df3bd635a597436e448b2de1407531f3a))
### Performance Improvements
* 阿里云 FC3.0 不在要求证书加密方式为旧版, 修复支持的协议类型可以正常选择 ([a34db74](https://github.com/certd/certd/commit/a34db7449eff6ad1dda01de673bf85579fa3865a))
* 部署到腾讯云cdn每个域名增加3每秒延迟 ([f7d43ad](https://github.com/certd/certd/commit/f7d43ad5af4663d4be369820a80d1fd9817ca4ab))
* 腾讯云关闭证书通知增加开关选项,在腾讯云授权里面 ([a77c777](https://github.com/certd/certd/commit/a77c777980dd38d97d983124eeed1596879bba95))
* 证书申请任务默认不发送申请成功通知 ([0283bd2](https://github.com/certd/certd/commit/0283bd2f978dbcd13d361129135e439dd9fbc180))
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Bug Fixes ### Bug Fixes

View File

@@ -1,6 +1,6 @@
# Certd # Certd
[English](./README_en.md) | [中文](./README.md) 中文 | [English](./README_en.md)
Certd® 是一个免费的全自动证书管理系统,让你的网站证书永不过期。 Certd® 是一个免费的全自动证书管理系统,让你的网站证书永不过期。
后缀d取自linux守护进程的命名风格意为证书守护进程 后缀d取自linux守护进程的命名风格意为证书守护进程

View File

@@ -1,6 +1,6 @@
# Certd # Certd
[English](./README_en.md) | [中文](./README.md) [中文](./README.md) | English
Certd® is a free, fully automated certificate management system that ensures your website certificates never expire. The suffix 'd' is inspired by the naming convention of Linux daemons, representing a certificate daemon. Certd® is a free, fully automated certificate management system that ensures your website certificates never expire. The suffix 'd' is inspired by the naming convention of Linux daemons, representing a certificate daemon.

View File

@@ -1 +1 @@
23:23 00:34

View File

@@ -45,7 +45,7 @@ services:
# 设置环境变量即可自定义certd配置 # 设置环境变量即可自定义certd配置
# 配置项见: packages/ui/certd-server/src/config/config.default.ts # 配置项见: packages/ui/certd-server/src/config/config.default.ts
# 配置规则: certd_ + 配置项, 点号用_代替 # 配置规则: certd_ + 配置项, 点号用_代替
# #↓↓↓↓ ----------------------------- 如果忘记管理员密码可以设置为true重启之后管理员密码将改成123456然后请及时修改回false # #↓↓↓↓ ----------------------------- 如果忘记管理员密码可以设置为truedocker compose up -d 重建容器之后管理员密码将改成123456然后请及时修改回false
- certd_system_resetAdminPasswd=false - certd_system_resetAdminPasswd=false
# 默认使用sqlite文件数据库如果需要使用其他数据库请设置以下环境变量 # 默认使用sqlite文件数据库如果需要使用其他数据库请设置以下环境变量

View File

@@ -107,7 +107,6 @@ export default defineConfig({
text: "常见问题", text: "常见问题",
items: [ items: [
{text: "QA", link: "/guide/qa/use.md"}, {text: "QA", link: "/guide/qa/use.md"},
{text: "常见报错处理", link: "/guide/qa/"},
{text: "群晖证书部署", link: "/guide/use/synology/"}, {text: "群晖证书部署", link: "/guide/use/synology/"},
{text: "腾讯云密钥获取", link: "/guide/use/tencent/"}, {text: "腾讯云密钥获取", link: "/guide/use/tencent/"},
{text: "连接windows主机", link: "/guide/use/host/windows.md"}, {text: "连接windows主机", link: "/guide/use/host/windows.md"},
@@ -120,6 +119,7 @@ export default defineConfig({
{text: "邮箱配置", link: "/guide/use/email/index.md"}, {text: "邮箱配置", link: "/guide/use/email/index.md"},
{text: "IPv6支持", link: "/guide/use/setting/ipv6.md"}, {text: "IPv6支持", link: "/guide/use/setting/ipv6.md"},
{text: "ESXi", link: "/guide/use/ESXi/index.md"}, {text: "ESXi", link: "/guide/use/ESXi/index.md"},
{text: "子域名托管", link: "/guide/use/cert/subdomain.md"},
] ]
}, },
{ {

View File

@@ -3,6 +3,83 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Bug Fixes
* 前置任务输出不存在时输出警告提示 ([b59052c](https://github.com/certd/certd/commit/b59052cc43b7b070fabd8b8e914e4c2a5e0ad61c))
* 修复批量流水线执行时日志显示错乱的问题 ([4372adc](https://github.com/certd/certd/commit/4372adc703b9a4c785664054ab2a533626d815a8))
* 修复远程数据选择无法过滤的bug ([6cbb073](https://github.com/certd/certd/commit/6cbb0739f8428d51b0712f718fe4d236cc087cf9))
* 修复mysql下购买套餐加量包无效的bug ([c26ad4c](https://github.com/certd/certd/commit/c26ad4c8075f0606d45b8da13915737968d6191a))
### Performance Improvements
* 创建证书时支持选择通知时机 ([0e96bfd](https://github.com/certd/certd/commit/0e96bfdfa377824d204e72923d1176408ae6b300))
* 创建k8s secret 时设置type为tls ([79ebabf](https://github.com/certd/certd/commit/79ebabfcfb9e5a534049c84f5f1a642b357fc856))
* 去掉宝塔url后面的斜杠 ([8a0c2b9](https://github.com/certd/certd/commit/8a0c2b9b13628da750c25757e0cb8ed3038775ba))
* 商业版隐藏文档相关链接 ([4443a1c](https://github.com/certd/certd/commit/4443a1c0308fa6b95a05efd73d15d24b65d641c9))
* 商业版隐藏文档相关链接 ([db89561](https://github.com/certd/certd/commit/db8956148083bc4f988226ccf719940d08158a27))
* 增加健康检查探针 /health/liveliness 和 /health/readiness ([44019e1](https://github.com/certd/certd/commit/44019e104289fedd32a867db00e9c6cb71b389cc))
* 支持根据id更新证书证书Id不变接口不过该接口为白名单功能普通腾讯云账户无法使用 ([fe9c4f3](https://github.com/certd/certd/commit/fe9c4f3391ff07c01dd9a252225f69a129c39050))
* 支持godaddy ([b7980aa](https://github.com/certd/certd/commit/b7980aad5ab50f58662eaddf5d84aa82876a98eb))
* 支持ssl.com证书颁发机构 ([27b6dfa](https://github.com/certd/certd/commit/27b6dfa4d2ab3bddd284c3a34511a72e1a513a4c))
* 子域名托管说明 ([39a0223](https://github.com/certd/certd/commit/39a02235cf4416bb5bd1acd3831241efeaa2f602))
* ssh 增加超时断开连接默认10分钟超时 ([c24a040](https://github.com/certd/certd/commit/c24a040c19cacafc79228d7a7649af93837d94a1))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Bug Fixes
* 更新我爱云CDN域名地址和部分目录结构 [@tyjsjxh](https://github.com/tyjsjxh) ([#514](https://github.com/certd/certd/issues/514)) ([78e7a81](https://github.com/certd/certd/commit/78e7a81638c2ee779f0ab6c3ba7e5c6f6e064151))
* 修复cron选择组件星期显示错误的bug ([eb75e52](https://github.com/certd/certd/commit/eb75e52278f94a72643f7317e6740fb42666c68a))
* 修复proxmox某些情况下执行卡住的bug ([ebd6917](https://github.com/certd/certd/commit/ebd6917a1d40ae4d94555c32b7e3c093d0599b94))
### Performance Improvements
* 部署到k8s支持自动创建secret ([c09c962](https://github.com/certd/certd/commit/c09c962cb676ca261610aa9f3e5105c9dae43f43))
* 短信验证码支持腾讯云 ([9108459](https://github.com/certd/certd/commit/9108459ae42bcd95a59acba164a64e82e5f2cfe6))
* 商业版支持自定义插件的参数配置 ([17f23f3](https://github.com/certd/certd/commit/17f23f37516af925d5049291d67d41e4271f81f8))
* 腾讯云插件支持国际版 ([58e82d5](https://github.com/certd/certd/commit/58e82d5dbd4ebf089ef239578ef9b68454d17b30))
* 腾讯云EO插件支持自动获取zoneid和域名列表 ([70fcdc9](https://github.com/certd/certd/commit/70fcdc9ebbfb7c883c0c8a2138f61a0776a9491b))
* 支持部署到阿里云云原生API网关、AI网关 ([2ca20be](https://github.com/certd/certd/commit/2ca20be197720201fceabcce9d927f4dbc1cc872))
* 支持部署到华为云obs ([9feb9d0](https://github.com/certd/certd/commit/9feb9d04b3c56ec95c06fcf4fd071eb0e88ffc6f))
* 支持部署到dokploy ([7dbdeae](https://github.com/certd/certd/commit/7dbdeaebe0bfee7521a863fe5e6b4a712aec5876))
* 支持删除宝塔证书夹中的过期证书 ([3575113](https://github.com/certd/certd/commit/3575113655be751d19f88c64491e98a89042d6a2))
* 支持p7b证书格式 ([d9f4a57](https://github.com/certd/certd/commit/d9f4a5793d68a017a5d80ad5385cbda603c4e165))
* lecdnv2支持api token ([e448934](https://github.com/certd/certd/commit/e4489343fee7754be07bcfc3323969dc3a30e90c))
* openapi返回证书时挑选匹配范围最小的那一个增加format参数增加返回值p7b格式增加detail返回 ([2085bcc](https://github.com/certd/certd/commit/2085bcceb61c3723c9bdfec4c4cc0917631ff5e5))
* ssh 配置sudo免密提示 ([e1e7011](https://github.com/certd/certd/commit/e1e7011853ad0c5bd7b09c3690861d5aa34b2db4))
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
### Bug Fixes
* 修复新部署的无法保存公共eab配置的bug ([d5dee75](https://github.com/certd/certd/commit/d5dee75df3bd635a597436e448b2de1407531f3a))
### Performance Improvements
* 阿里云 FC3.0 不在要求证书加密方式为旧版, 修复支持的协议类型可以正常选择 ([a34db74](https://github.com/certd/certd/commit/a34db7449eff6ad1dda01de673bf85579fa3865a))
* 部署到腾讯云cdn每个域名增加3每秒延迟 ([f7d43ad](https://github.com/certd/certd/commit/f7d43ad5af4663d4be369820a80d1fd9817ca4ab))
* 腾讯云关闭证书通知增加开关选项,在腾讯云授权里面 ([a77c777](https://github.com/certd/certd/commit/a77c777980dd38d97d983124eeed1596879bba95))
* 证书申请任务默认不发送申请成功通知 ([0283bd2](https://github.com/certd/certd/commit/0283bd2f978dbcd13d361129135e439dd9fbc180))
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Bug Fixes
* 修复授权配置复制功能,无法复制已加密字段的问题 ([221e068](https://github.com/certd/certd/commit/221e068bac3af6cd5d1794f8cd4c2ec5c0bc3f45))
### Performance Improvements
* 百度云支持上传到证书托管,支持部署到负载均衡 ([798a48a](https://github.com/certd/certd/commit/798a48aa9686fd5d11cfffb6cd93eadfc40aacb3))
* 部署到百度cdn支持自动获取域名列表选择 ([4e432ed](https://github.com/certd/certd/commit/4e432ed03f4fb564e85a2f284ee26b58400b82f5))
* 验证码可重试次数设置为3次 ([1bdceee](https://github.com/certd/certd/commit/1bdceeecf4b5daecdd621a05a2596b6eb45ce8ea))
* 增加找回密码的验证码可重试次数 [@nicheng-he](https://github.com/nicheng-he) ([#496](https://github.com/certd/certd/issues/496)) ([fe03f99](https://github.com/certd/certd/commit/fe03f9942b5662fb90cad86da10782f5dc3603f5))
* 支持阿里云API网关 ([9e1e4ee](https://github.com/certd/certd/commit/9e1e4eeec2859759ca5b07834c9d24cf88a6ad33))
* 支持部署到金山云CDN ([dfa74a6](https://github.com/certd/certd/commit/dfa74a69f7cbb9009d3e20c7eecfa1b905a00cf0))
* 支持更新金山云cdn证书 ([462e22a](https://github.com/certd/certd/commit/462e22a3b0a94887462fe6aa68e4671a365e0737))
* 支持apisix证书部署 ([9b63fb4](https://github.com/certd/certd/commit/9b63fb4ee2c6b56139160c5bf63482dab0869c2b))
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07) ## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
### Bug Fixes ### Bug Fixes

View File

@@ -58,3 +58,18 @@ kill -9 $(lsof -t -i:7001)
## 四、备份恢复 ## 四、备份恢复
将备份的`db.sqlite`及同目录下的其他文件覆盖到原来的位置重启certd即可 将备份的`db.sqlite`及同目录下的其他文件覆盖到原来的位置重启certd即可
## 六、常见问题
### 1. npm install better-sqlite3 时提示node-gyp需要vscode环境编译
1. 首先确保node版本为22以上
2. 将下面两行加到 ~/.npmrc 里面
3. 重新install
> better_sqlite3_binary_host=https://registry.npmmirror.com/-/binary/better-sqlite3
> better_sqlite3_binary_host_mirror=https://registry.npmmirror.com/-/binary/better-sqlite3

View File

@@ -1,81 +0,0 @@
# 常见报错解决
## 1. getaddrinfo ENOTFOUND错误
如果出现`getaddrinfo ENOTFOUND`/`getaddrinfo EAI_AGAIN`错误,可以尝试在`docker-compose.yaml`中设置dns
```yaml
version: '3.3' # 兼容旧版docker-compose
services:
certd:
#↓↓↓↓ ------------ # 如果出现getaddrinfo ENOTFOUND 或 EAI_AGAIN错误可以尝试设置dns
dns:
- 223.5.5.5 # 阿里云公共dns
- 223.6.6.6
# # ↓↓↓↓ ------- # 如果你服务器在腾讯云可以用这个替换上面阿里云的公共dns
# - 119.29.29.29 # 腾讯云公共dns
# - 182.254.116.116
# # ↓↓↓↓ ------- # 如果你服务器部署在国外可以用这个替换上面阿里云的公共dns
# - 8.8.8.8 # 谷歌公共dns
# - 8.8.4.4
```
如果仍然有问题,按如下步骤检查是否能够ping通域名
```shell
docker exec -it certd /bin/sh
ping www.baidu.com
ping gg.px.certd.handfree.work
ping app.handfree.work
```
如果您是宝塔部署的
可以试试将容器网络加入brige网络看是否解决问题
![img.png](images/baota-net.png)
如果还是不行,请联系我们
## 2. 连接IPv6超时
docker-compose 需要放开IPv6网络的配置
```yaml
services:
certd:
networks:
- ip6net
# ↓↓↓↓ -------------------------------------------------------------- 启用ipv6网络还需要把上面networks的注释放开
networks:
ip6net:
enable_ipv6: true
ipam:
config:
- subnet: 2001:db8::/64
```
## 3. SSL_CERT_NOT_MATCH_DOMAIN_ERROR
部署证书任务报类似 `SSL_CERT_NOT_MATCH_DOMAIN_ERROR`错误
这是由于当前流水线的证书域名与要部署的目标站点的域名不匹配导致的,在申请证书任务中,增加目标站点域名,重新运行流水线即可
## 4. 没有服务器配置文件,请检查是否开启了外网映射!
宝塔网站证书部署报错:`Error: 没有服务器配置文件,请检查是否开启了外网映射!`
解决方案:先手动在宝塔网站中设置一次证书
## 5. 如何查看容器日志
```shell
docker logs -f --tail 200 certd
```
## 6. 容器内走时不准,或者时区不对
走时不准确,慢慢偏差越来越大
或者整个时区都不对
可以尝试挂载localtime文件
```yaml
volumes:
# ↓↓↓↓↓ -------------------- 如果走时不准请尝试挂载localtime文件
- /etc/localtime:/etc/localtime
- /etc/timezone:/etc/timezone
```

View File

@@ -1,4 +1,4 @@
# 使用问题 # 常见问题
## 1. 是否支持IP证书 ## 1. 是否支持IP证书
@@ -12,3 +12,9 @@
当证书到期前35天创建流水线时可以修改将会自动重新申请证书自动部署 当证书到期前35天创建流水线时可以修改将会自动重新申请证书自动部署
## 3. too many certificates 错误
当出现如下报错时说明相同的域名短时间内申请超过5次
解决方案:可以加多一个子域名,重新执行就可以规避次错误
```
"detail": too many certificates (5) already issued for this exact set of idantifiers in the last 168hm0s
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -0,0 +1,10 @@
# 二级子域名托管
如果你的域名是免费的二级域名比如sub.handsfree.work托管在CF或者阿里云上
在使用DNS方式校验时需要设置子域名托管
[阿里云子域名托管说明](https://help.aliyun.com/zh/dns/pubz-subdomain-management)
![img.png](./images/subdomain1.png)
![img_1.png](./images/subdomain2.png)

View File

@@ -9,5 +9,5 @@
} }
}, },
"npmClient": "pnpm", "npmClient": "pnpm",
"version": "1.36.16" "version": "1.36.19"
} }

View File

@@ -31,7 +31,7 @@
"init": "lerna run build", "init": "lerna run build",
"init:dev": "lerna run build", "init:dev": "lerna run build",
"docs:dev": "vitepress dev docs", "docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs", "docs:build": "npm run copylogs && vitepress build docs",
"docs:preview": "vitepress preview docs", "docs:preview": "vitepress preview docs",
"pub": "echo 1" "pub": "echo 1"
}, },

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/publishlab/node-acme-client/compare/v1.36.18...v1.36.19) (2025-09-05)
### Performance Improvements
* 支持ssl.com证书颁发机构 ([27b6dfa](https://github.com/publishlab/node-acme-client/commit/27b6dfa4d2ab3bddd284c3a34511a72e1a513a4c))
## [1.36.18](https://github.com/publishlab/node-acme-client/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/acme-client
## [1.36.17](https://github.com/publishlab/node-acme-client/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/acme-client
## [1.36.16](https://github.com/publishlab/node-acme-client/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/publishlab/node-acme-client/compare/v1.36.15...v1.36.16) (2025-08-16)
### Performance Improvements ### Performance Improvements

View File

@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client", "description": "Simple and unopinionated ACME client",
"private": false, "private": false,
"author": "nmorsman", "author": "nmorsman",
"version": "1.36.16", "version": "1.36.19",
"type": "module", "type": "module",
"module": "scr/index.js", "module": "scr/index.js",
"main": "src/index.js", "main": "src/index.js",
@@ -18,7 +18,7 @@
"types" "types"
], ],
"dependencies": { "dependencies": {
"@certd/basic": "^1.36.16", "@certd/basic": "^1.36.19",
"@peculiar/x509": "^1.11.0", "@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.5",
"axios": "^1.7.2", "axios": "^1.7.2",
@@ -69,5 +69,5 @@
"bugs": { "bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues" "url": "https://github.com/publishlab/node-acme-client/issues"
}, },
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00" "gitHead": "6d8981479517b5de9634e242c1ebf22e70527ec4"
} }

View File

@@ -25,6 +25,10 @@ export const directory = {
staging: 'https://acme.zerossl.com/v2/DV90', staging: 'https://acme.zerossl.com/v2/DV90',
production: 'https://acme.zerossl.com/v2/DV90', production: 'https://acme.zerossl.com/v2/DV90',
}, },
sslcom:{
staging: 'https://acme.ssl.com/sslcom-dv-rsa',
production: 'https://acme.ssl.com/sslcom-dv-rsa',
}
}; };
/** /**

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Bug Fixes
* 修复批量流水线执行时日志显示错乱的问题 ([4372adc](https://github.com/certd/certd/commit/4372adc703b9a4c785664054ab2a533626d815a8))
### Performance Improvements
* 去掉宝塔url后面的斜杠 ([8a0c2b9](https://github.com/certd/certd/commit/8a0c2b9b13628da750c25757e0cb8ed3038775ba))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/basic
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/basic
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/basic **Note:** Version bump only for package @certd/basic

View File

@@ -1 +1 @@
12:46 00:30

View File

@@ -1,7 +1,7 @@
{ {
"name": "@certd/basic", "name": "@certd/basic",
"private": false, "private": false,
"version": "1.36.16", "version": "1.36.19",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -45,5 +45,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00" "gitHead": "6d8981479517b5de9634e242c1ebf22e70527ec4"
} }

View File

@@ -1,22 +1,4 @@
import log4js, { LoggingEvent, Logger } from "log4js"; import log4js, { CallStack, Level } from "log4js";
const OutputAppender = {
configure: (config: any, layouts: any, findAppender: any, levels: any) => {
let layout = layouts.basicLayout;
if (config.layout) {
layout = layouts.layout(config.layout.type, config.layout);
}
function customAppender(layout: any, timezoneOffset: any) {
return (loggingEvent: LoggingEvent) => {
if (loggingEvent.context.outputHandler?.write) {
const text = `${layout(loggingEvent, timezoneOffset)}\n`;
loggingEvent.context.outputHandler.write(text);
}
};
}
return customAppender(layout, config.timezoneOffset);
},
};
let logFilePath = "./logs/app.log"; let logFilePath = "./logs/app.log";
export function resetLogConfigure() { export function resetLogConfigure() {
@@ -24,7 +6,6 @@ export function resetLogConfigure() {
log4js.configure({ log4js.configure({
appenders: { appenders: {
std: { type: "stdout" }, std: { type: "stdout" },
output: { type: OutputAppender },
file: { file: {
type: "dateFile", type: "dateFile",
filename: logFilePath, filename: logFilePath,
@@ -33,7 +14,7 @@ export function resetLogConfigure() {
numBackups: 3, numBackups: 3,
}, },
}, },
categories: { default: { appenders: ["std", "file"], level: "info" }, pipeline: { appenders: ["std", "file", "output"], level: "info" } }, categories: { default: { appenders: ["std", "file"], level: "info" }, pipeline: { appenders: ["std", "file"], level: "info" } },
}); });
} }
resetLogConfigure(); resetLogConfigure();
@@ -44,15 +25,98 @@ export function resetLogFilePath(filePath: string) {
resetLogConfigure(); resetLogConfigure();
} }
export function buildLogger(write: (text: string) => void) { export function buildLogger(write: (text: string) => void) {
const logger = log4js.getLogger("pipeline"); return new PipelineLogger("pipeline", write);
const _secrets: string[] = []; }
//@ts-ignore
logger.addSecret = (secret: string) => { export type ILogger = {
_secrets.push(secret); readonly category: string;
}; level: Level | string;
logger.addContext("outputHandler", { log(level: Level | string, ...args: any[]): void;
write: (text: string) => {
for (const item of _secrets) { isLevelEnabled(level?: string): boolean;
isTraceEnabled(): boolean;
isDebugEnabled(): boolean;
isInfoEnabled(): boolean;
isWarnEnabled(): boolean;
isErrorEnabled(): boolean;
isFatalEnabled(): boolean;
_log(level: Level, data: any): void;
addContext(key: string, value: any): void;
removeContext(key: string): void;
clearContext(): void;
/**
* Replace the basic parse function with a new custom one
* - Note that linesToSkip will be based on the origin of the Error object in addition to the callStackLinesToSkip (at least 1)
* @param parseFunction the new parseFunction. Use `undefined` to reset to the base implementation
*/
setParseCallStackFunction(parseFunction: (error: Error, linesToSkip: number) => CallStack | undefined): void;
/**
* Adjust the value of linesToSkip when the parseFunction is called.
*
* Cannot be less than 0.
*/
callStackLinesToSkip: number;
trace(message: any, ...args: any[]): void;
debug(message: any, ...args: any[]): void;
info(message: any, ...args: any[]): void;
warn(message: any, ...args: any[]): void;
error(message: any, ...args: any[]): void;
fatal(message: any, ...args: any[]): void;
mark(message: any, ...args: any[]): void;
};
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
const formatter = new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
function formatDateIntl(date = new Date()) {
const milliseconds = date.getMilliseconds(); // 获取毫秒
const formattedMilliseconds = milliseconds.toString().padStart(3, "0");
return formatter.format(date) + "." + formattedMilliseconds;
}
// @ts-ignore
export class PipelineLogger implements ILogger {
callStackLinesToSkip: number = 3;
readonly category: string = "pipeline";
level: Level | string = "info";
_secrets: string[] = [];
logger: ILogger;
customWriter!: (text: string) => void;
constructor(name: string, write: (text: string) => void) {
this.customWriter = write;
this.logger = log4js.getLogger(name);
}
addSecret(secret: string) {
this._secrets.push(secret);
}
_doLog(level: string, ...args: any[]) {
let text = args.join(" ");
if (this.customWriter) {
for (const item of this._secrets) {
if (item == null) { if (item == null) {
continue; continue;
} }
@@ -66,10 +130,88 @@ export function buildLogger(write: (text: string) => void) {
text = text.replaceAll(item, "*".repeat(item.length)); text = text.replaceAll(item, "*".repeat(item.length));
} }
} }
write(text); text = `[${formatDateIntl()}] [${level.toUpperCase()}] - ${text} \n`;
}, this.customWriter(text);
}); }
return logger; // @ts-ignore
} this.logger[level](...args);
}
export type ILogger = Logger; _log(level: Level, data: any): void {}
addContext(key: string, value: any): void {}
clearContext(): void {}
debug(message: any, ...args: any[]): void {
if (this.isDebugEnabled()) {
this._doLog("debug", message, ...args);
}
}
error(message: any, ...args: any[]): void {
if (this.isErrorEnabled()) {
this._doLog("error", message, ...args);
}
}
fatal(message: any, ...args: any[]): void {
if (this.isFatalEnabled()) {
this._doLog("fatal", message, ...args);
}
}
info(message: any, ...args: any[]): void {
if (this.isInfoEnabled()) {
this._doLog("info", message, ...args);
}
}
trace(message: any, ...args: any[]): void {
if (this.isTraceEnabled()) {
this._doLog("trace", message, ...args);
}
}
warn(message: any, ...args: any[]): void {
if (this.isWarnEnabled()) {
this._doLog("warn", message, ...args);
}
}
isDebugEnabled(): boolean {
return logger.isDebugEnabled();
}
isErrorEnabled(): boolean {
return logger.isErrorEnabled();
}
isFatalEnabled(): boolean {
return logger.isFatalEnabled();
}
isInfoEnabled(): boolean {
return logger.isInfoEnabled();
}
isLevelEnabled(level?: string): boolean {
return logger.isLevelEnabled();
}
isTraceEnabled(): boolean {
return logger.isTraceEnabled();
}
isWarnEnabled(): boolean {
return logger.isWarnEnabled();
}
log(level: Level | string, ...args: any[]): void {}
mark(message: any, ...args: any[]): void {}
removeContext(key: string): void {}
setParseCallStackFunction(parseFunction: (error: Error, linesToSkip: number) => CallStack | undefined): void {}
}

View File

@@ -1,6 +1,5 @@
import axios, { AxiosHeaders, AxiosRequestConfig } from "axios"; import axios, { AxiosHeaders, AxiosRequestConfig } from "axios";
import { ILogger, logger } from "./util.log.js"; import { ILogger, logger } from "./util.log.js";
import { Logger } from "log4js";
import { HttpProxyAgent } from "http-proxy-agent"; import { HttpProxyAgent } from "http-proxy-agent";
import { HttpsProxyAgent } from "https-proxy-agent"; import { HttpsProxyAgent } from "https-proxy-agent";
import nodeHttp from "http"; import nodeHttp from "http";
@@ -84,7 +83,7 @@ export function getGlobalAgents() {
/** /**
* @description 创建请求实例 * @description 创建请求实例
*/ */
export function createAxiosService({ logger }: { logger: Logger }) { export function createAxiosService({ logger }: { logger: ILogger }) {
// 创建一个 axios 实例 // 创建一个 axios 实例
const service = axios.create(); const service = axios.create();

View File

@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Bug Fixes
* 前置任务输出不存在时输出警告提示 ([b59052c](https://github.com/certd/certd/commit/b59052cc43b7b070fabd8b8e914e4c2a5e0ad61c))
### Performance Improvements
* 支持godaddy ([b7980aa](https://github.com/certd/certd/commit/b7980aad5ab50f58662eaddf5d84aa82876a98eb))
* 支持ssl.com证书颁发机构 ([27b6dfa](https://github.com/certd/certd/commit/27b6dfa4d2ab3bddd284c3a34511a72e1a513a4c))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/pipeline
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/pipeline
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Performance Improvements ### Performance Improvements

View File

@@ -1,7 +1,7 @@
{ {
"name": "@certd/pipeline", "name": "@certd/pipeline",
"private": false, "private": false,
"version": "1.36.16", "version": "1.36.19",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -17,8 +17,8 @@
"pub": "npm publish" "pub": "npm publish"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.36.16", "@certd/basic": "^1.36.19",
"@certd/plus-core": "^1.36.16", "@certd/plus-core": "^1.36.19",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13" "reflect-metadata": "^0.1.13"
@@ -44,5 +44,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00" "gitHead": "6d8981479517b5de9634e242c1ebf22e70527ec4"
} }

View File

@@ -21,9 +21,9 @@ export type PageRes = {
export class Pager { export class Pager {
pageNo: number; pageNo: number;
pageSize: number; pageSize: number;
constructor(req: PageSearch) { constructor(req?: PageSearch) {
this.pageNo = req.pageNo ?? 1; this.pageNo = req?.pageNo ?? 1;
this.pageSize = req.pageSize || 50; this.pageSize = req?.pageSize || 50;
} }
getOffset() { getOffset() {

View File

@@ -314,7 +314,7 @@ export class Executor {
const outputKey = arr[2]; const outputKey = arr[2];
input[key] = this.currentStatusMap.get(id)?.status?.output[outputKey] ?? this.lastStatusMap.get(id)?.status?.output[outputKey]; input[key] = this.currentStatusMap.get(id)?.status?.output[outputKey] ?? this.lastStatusMap.get(id)?.status?.output[outputKey];
if (input[key] == null) { if (input[key] == null) {
this.logger.warn(`${item.title}的配置未找到对应的输出值,请确认对应的前置任务是否存在或者是否执行正确`); currentLogger.warn(`${item.title}的配置未找到对应的输出值,请确认对应的前置任务是否存在或者是否执行正确`);
} }
} }
} }

View File

@@ -253,9 +253,9 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
return name + "_" + dayjs().format("YYYYMMDDHHmmssSSS"); return name + "_" + dayjs().format("YYYYMMDDHHmmssSSS");
} }
buildCertName(domain: string) { buildCertName(domain: string, prefix = "") {
domain = domain.replaceAll("*", "_").replaceAll(".", "_"); domain = domain.replaceAll("*", "_").replaceAll(".", "_");
return `${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`; return `${prefix}_${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`;
} }
async onRequest(req: PluginRequestHandleReq<any>) { async onRequest(req: PluginRequestHandleReq<any>) {

View File

@@ -69,9 +69,15 @@ export class Registry<T = any> {
return this.storage; return this.storage;
} }
getDefineList() { getDefineList(prefix?: string) {
let list = []; let list = [];
if (prefix) {
prefix = prefix + ":";
}
for (const key in this.storage) { for (const key in this.storage) {
if (prefix && !key.startsWith(prefix)) {
continue;
}
const define = this.getDefine(key); const define = this.getDefine(key);
if (define) { if (define) {
if (define?.deprecated) { if (define?.deprecated) {
@@ -90,7 +96,10 @@ export class Registry<T = any> {
return list; return list;
} }
getDefine(key: string) { getDefine(key: string, prefix?: string) {
if (prefix) {
key = prefix + ":" + key;
}
const item = this.storage[key]; const item = this.storage[key];
if (!item) { if (!item) {
return; return;

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
**Note:** Version bump only for package @certd/lib-huawei
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/lib-huawei
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/lib-huawei
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/lib-huawei **Note:** Version bump only for package @certd/lib-huawei

View File

@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-huawei", "name": "@certd/lib-huawei",
"private": false, "private": false,
"version": "1.36.16", "version": "1.36.19",
"main": "./dist/bundle.js", "main": "./dist/bundle.js",
"module": "./dist/bundle.js", "module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts", "types": "./dist/d/index.d.ts",
@@ -24,5 +24,5 @@
"prettier": "^2.8.8", "prettier": "^2.8.8",
"tslib": "^2.8.1" "tslib": "^2.8.1"
}, },
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00" "gitHead": "6d8981479517b5de9634e242c1ebf22e70527ec4"
} }

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
**Note:** Version bump only for package @certd/lib-iframe
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/lib-iframe
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/lib-iframe
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/lib-iframe **Note:** Version bump only for package @certd/lib-iframe

View File

@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-iframe", "name": "@certd/lib-iframe",
"private": false, "private": false,
"version": "1.36.16", "version": "1.36.19",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -31,5 +31,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00" "gitHead": "6d8981479517b5de9634e242c1ebf22e70527ec4"
} }

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
**Note:** Version bump only for package @certd/jdcloud
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/jdcloud
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/jdcloud
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/jdcloud **Note:** Version bump only for package @certd/jdcloud

View File

@@ -1,6 +1,6 @@
{ {
"name": "@certd/jdcloud", "name": "@certd/jdcloud",
"version": "1.36.16", "version": "1.36.19",
"description": "jdcloud openApi sdk", "description": "jdcloud openApi sdk",
"main": "./dist/bundle.js", "main": "./dist/bundle.js",
"module": "./dist/bundle.js", "module": "./dist/bundle.js",
@@ -61,5 +61,5 @@
"fetch" "fetch"
] ]
}, },
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00" "gitHead": "6d8981479517b5de9634e242c1ebf22e70527ec4"
} }

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Bug Fixes
* 修复远程数据选择无法过滤的bug ([6cbb073](https://github.com/certd/certd/commit/6cbb0739f8428d51b0712f718fe4d236cc087cf9))
### Performance Improvements
* 创建k8s secret 时设置type为tls ([79ebabf](https://github.com/certd/certd/commit/79ebabfcfb9e5a534049c84f5f1a642b357fc856))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Performance Improvements
* 部署到k8s支持自动创建secret ([c09c962](https://github.com/certd/certd/commit/c09c962cb676ca261610aa9f3e5105c9dae43f43))
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/lib-k8s
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/lib-k8s **Note:** Version bump only for package @certd/lib-k8s

View File

@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-k8s", "name": "@certd/lib-k8s",
"private": false, "private": false,
"version": "1.36.16", "version": "1.36.19",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -17,7 +17,7 @@
"pub": "npm publish" "pub": "npm publish"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.36.16", "@certd/basic": "^1.36.19",
"@kubernetes/client-node": "0.21.0" "@kubernetes/client-node": "0.21.0"
}, },
"devDependencies": { "devDependencies": {
@@ -32,5 +32,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00" "gitHead": "6d8981479517b5de9634e242c1ebf22e70527ec4"
} }

View File

@@ -1,7 +1,7 @@
import { CoreV1Api, KubeConfig, NetworkingV1Api, V1Ingress, V1Secret } from "@kubernetes/client-node"; import { CoreV1Api, KubeConfig, NetworkingV1Api, V1Ingress, V1Secret } from "@kubernetes/client-node";
import dns from "dns"; import dns from "dns";
import { ILogger } from "@certd/basic"; import { ILogger } from "@certd/basic";
import _ from "lodash-es"; import { merge } from "lodash-es";
export type K8sClientOpts = { export type K8sClientOpts = {
kubeConfigStr: string; kubeConfigStr: string;
@@ -85,12 +85,11 @@ export class K8sClient {
/** /**
* 创建Secret * 创建Secret
* @param opts {namespace:default, body:yamlStr} * @param opts {namespace:default, body:yamlStr}
* @returns {Promise<*>}
*/ */
async createSecret(opts: { namespace: string; body: V1Secret }) { async createSecret(opts: { namespace: string; body: V1Secret }) {
const namespace = opts.namespace || "default"; const namespace = opts.namespace || "default";
const created = await this.client.createNamespacedSecret(namespace, opts.body); const created = await this.client.createNamespacedSecret(namespace, opts.body);
this.logger.info("new secrets:", opts.body); this.logger.info("new secrets:", opts.body.metadata);
return created.body; return created.body;
} }
@@ -103,17 +102,39 @@ export class K8sClient {
// return await this.client.replaceNamespacedSecret(secretName, namespace, opts.body); // return await this.client.replaceNamespacedSecret(secretName, namespace, opts.body);
// } // }
async patchSecret(opts: { namespace: string; secretName: string; body: V1Secret }) { async patchSecret(opts: { namespace: string; secretName: string; body: V1Secret; createOnNotFound?: boolean }) {
const namespace = opts.namespace || "default"; const namespace = opts.namespace || "default";
const secretName = opts.secretName; const secretName = opts.secretName;
if (secretName == null) { if (secretName == null) {
throw new Error("secretName 不能为空"); throw new Error("secretName 不能为空");
} }
this.logger.info("patch secret:", secretName, namespace); this.logger.info("patch secret:", secretName, namespace);
const oldSecret = await this.client.readNamespacedSecret(secretName, namespace); let oldSecret: any = null;
const newSecret = _.merge(oldSecret.body, opts.body); try {
oldSecret = await this.client.readNamespacedSecret(secretName, namespace);
} catch (e) {
//@ts-ignore
if (e.response?.body?.code === 404) {
this.logger.warn(`secret ${secretName} 不存在`);
if (opts.createOnNotFound) {
//没有找到,则创建
const body = merge(
{
type: "kubernetes.io/tls",
},
opts.body
);
const res = await this.createSecret({ namespace, body });
this.logger.info(`secret ${secretName} 已创建`);
return res;
}
}
throw e;
}
const newSecret = merge(oldSecret.body, opts.body);
const res = await this.client.replaceNamespacedSecret(secretName, namespace, newSecret); const res = await this.client.replaceNamespacedSecret(secretName, namespace, newSecret);
this.logger.info("secret updated"); this.logger.info(`secret ${secretName} 已更新`);
return res.body; return res.body;
} }
@@ -145,7 +166,7 @@ export class K8sClient {
this.logger.info("patch ingress:", ingressName, namespace); this.logger.info("patch ingress:", ingressName, namespace);
const client = this.kubeconfig.makeApiClient(NetworkingV1Api); const client = this.kubeconfig.makeApiClient(NetworkingV1Api);
const oldIngress = await client.readNamespacedIngress(ingressName, namespace); const oldIngress = await client.readNamespacedIngress(ingressName, namespace);
const newIngress = _.merge(oldIngress.body, opts.body); const newIngress = merge(oldIngress.body, opts.body);
const res = await client.replaceNamespacedIngress(ingressName, namespace, newIngress); const res = await client.replaceNamespacedIngress(ingressName, namespace, newIngress);
this.logger.info("ingress patched", opts.body); this.logger.info("ingress patched", opts.body);
return res; return res;

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
**Note:** Version bump only for package @certd/lib-server
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/lib-server
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/lib-server
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Bug Fixes ### Bug Fixes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@certd/lib-server", "name": "@certd/lib-server",
"version": "1.36.16", "version": "1.36.19",
"description": "midway with flyway, sql upgrade way ", "description": "midway with flyway, sql upgrade way ",
"private": false, "private": false,
"type": "module", "type": "module",
@@ -27,10 +27,10 @@
], ],
"license": "AGPL", "license": "AGPL",
"dependencies": { "dependencies": {
"@certd/acme-client": "^1.36.16", "@certd/acme-client": "^1.36.19",
"@certd/basic": "^1.36.16", "@certd/basic": "^1.36.19",
"@certd/pipeline": "^1.36.16", "@certd/pipeline": "^1.36.19",
"@certd/plus-core": "^1.36.16", "@certd/plus-core": "^1.36.19",
"@midwayjs/cache": "~3.14.0", "@midwayjs/cache": "~3.14.0",
"@midwayjs/core": "~3.20.3", "@midwayjs/core": "~3.20.3",
"@midwayjs/i18n": "~3.20.3", "@midwayjs/i18n": "~3.20.3",
@@ -61,5 +61,5 @@
"typeorm": "^0.3.11", "typeorm": "^0.3.11",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00" "gitHead": "6d8981479517b5de9634e242c1ebf22e70527ec4"
} }

View File

@@ -1,8 +1,9 @@
import { SysSettingsEntity } from './system/index.js'; import { SysSettingsEntity } from './system/index.js';
import { AccessEntity } from './user/access/entity/access.js'; import { AccessEntity } from './user/access/entity/access.js';
import { AddonEntity } from "./user/index.js";
export * from './basic/index.js'; export * from './basic/index.js';
export * from './system/index.js'; export * from './system/index.js';
export * from './user/index.js'; export * from './user/index.js';
export { LibServerConfiguration as Configuration } from './configuration.js'; export { LibServerConfiguration as Configuration } from './configuration.js';
export const libServerEntities = [SysSettingsEntity, AccessEntity]; export const libServerEntities = [SysSettingsEntity, AccessEntity,AddonEntity];

View File

@@ -30,6 +30,13 @@ export class SysPublicSettings extends BaseSettings {
mpsNo?: string; mpsNo?: string;
robots?: boolean = true; robots?: boolean = true;
aiChatEnabled = true; aiChatEnabled = true;
//验证码是否开启
captchaEnabled = false;
//验证码类型
captchaType?: string;
captchaAddonId?:number;
} }
export class SysPrivateSettings extends BaseSettings { export class SysPrivateSettings extends BaseSettings {
@@ -207,4 +214,3 @@ export class SysSafeSetting extends BaseSettings {
}; };
} }

View File

@@ -0,0 +1,96 @@
import { HttpClient, ILogger, utils } from "@certd/basic";
import {upperFirst} from "lodash-es";
import { FormItemProps, PluginRequestHandleReq, Registrable } from "@certd/pipeline";
export type AddonRequestHandleReqInput<T = any> = {
id?: number;
title?: string;
addon: T;
};
export type AddonRequestHandleReq<T = any> = {
addonType: string;
} &PluginRequestHandleReq<AddonRequestHandleReqInput<T>>;
export type AddonInputDefine = FormItemProps & {
title: string;
required?: boolean;
};
export type AddonDefine = Registrable & {
addonType: string;
needPlus?: boolean;
input?: {
[key: string]: AddonInputDefine;
};
};
export type AddonInstanceConfig = {
id: number;
addonType: string;
type: string;
name: string;
userId: number;
setting: {
[key: string]: any;
};
};
export interface IAddon {
ctx: AddonContext;
[key: string]: any;
}
export type AddonContext = {
http: HttpClient;
logger: ILogger;
utils: typeof utils;
};
export abstract class BaseAddon implements IAddon {
define!: AddonDefine;
ctx!: AddonContext;
http!: HttpClient;
logger!: ILogger;
// eslint-disable-next-line @typescript-eslint/no-empty-function
async onInstance() {}
setCtx(ctx: AddonContext) {
this.ctx = ctx;
this.http = ctx.http;
this.logger = ctx.logger;
}
setDefine = (define:AddonDefine) => {
this.define = define;
};
async onRequest(req:AddonRequestHandleReq) {
if (!req.action) {
throw new Error("action is required");
}
let methodName = req.action;
if (!req.action.startsWith("on")) {
methodName = `on${upperFirst(req.action)}`;
}
// @ts-ignore
const method = this[methodName];
if (method) {
// @ts-ignore
return await this[methodName](req.data);
}
throw new Error(`action ${req.action} not found`);
}
}
export interface IAddonGetter {
getById<T = any>(id: any): Promise<T>;
getCommonById<T = any>(id: any): Promise<T>;
}

View File

@@ -0,0 +1,65 @@
// src/decorator/memoryCache.decorator.ts
import * as _ from "lodash-es";
import { merge } from "lodash-es";
import { addonRegistry } from "./registry.js";
import { AddonContext, AddonDefine, AddonInputDefine } from "./api.js";
import { Decorator } from "@certd/pipeline";
// 提供一个唯一 key
export const ADDON_CLASS_KEY = "pipeline:addon";
export const ADDON_INPUT_KEY = "pipeline:addon:input";
export function IsAddon(define: AddonDefine): ClassDecorator {
return (target: any) => {
target = Decorator.target(target);
const inputs: any = {};
const properties = Decorator.getClassProperties(target);
for (const property in properties) {
const input = Reflect.getMetadata(ADDON_INPUT_KEY, target, property);
if (input) {
inputs[property] = input;
}
}
_.merge(define, { input: inputs });
Reflect.defineMetadata(ADDON_CLASS_KEY, define, target);
target.define = define;
const key = `${define.addonType}:${define.name}`;
addonRegistry.register(key, {
define,
target: async () => {
return target;
},
});
};
}
export function AddonInput(input?: AddonInputDefine): PropertyDecorator {
return (target, propertyKey) => {
target = Decorator.target(target, propertyKey);
// const _type = Reflect.getMetadata("design:type", target, propertyKey);
Reflect.defineMetadata(ADDON_INPUT_KEY, input, target, propertyKey);
};
}
export async function newAddon(addonType:string,type: string, input: any, ctx: AddonContext) {
const key = `${addonType}:${type}`
const register = addonRegistry.get(key);
if (register == null) {
throw new Error(`${addonType} ${type} not found`);
}
// @ts-ignore
const pluginCls = await register.target();
// @ts-ignore
const plugin = new pluginCls();
merge(plugin, input);
if (!ctx) {
throw new Error("ctx is required");
}
plugin.setDefine(register.define);
plugin.setCtx(ctx);
await plugin.onInstance();
return plugin;
}

View File

@@ -0,0 +1,3 @@
export * from "./api.js";
export * from "./registry.js";
export * from "./decorator.js";

View File

@@ -0,0 +1,3 @@
import { createRegistry } from "@certd/pipeline";
export const addonRegistry = createRegistry("addon");

View File

@@ -0,0 +1,44 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
/**
*/
@Entity('cd_addon')
export class AddonEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
userId: number;
@Column({ comment: '名称', length: 100 })
name: string;
@Column({ name: 'addon_type', comment: 'addon类型', length: 100 })
addonType: string;
@Column({ comment: '类型', length: 100 })
type: string;
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
setting: string;
@Column({ name: 'is_system', comment: '是否系统级别', nullable: false, default: false })
isSystem: boolean;
@Column({ name: 'is_default', comment: '是否默认', nullable: false, default: false })
isDefault: boolean;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
})
updateTime: Date;
}

View File

@@ -0,0 +1,5 @@
export * from './api/index.js'
export * from './entity/addon.js'
export * from './service/addon-service.js'
export * from './service/addon-getter.js'
export * from './service/addon-sys-getter.js'

View File

@@ -0,0 +1,18 @@
import { IAddonGetter } from "../api/index.js";
export class AddonGetter implements IAddonGetter {
userId: number;
getter: <T>(id: any, userId?: number) => Promise<T>;
constructor(userId: number, getter: (id: any, userId: number) => Promise<any>) {
this.userId = userId;
this.getter = getter;
}
async getById<T = any>(id: any) {
return await this.getter<T>(id, this.userId);
}
async getCommonById<T = any>(id: any) {
return await this.getter<T>(id, 0);
}
}

View File

@@ -0,0 +1,231 @@
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, Repository } from "typeorm";
import { AddonDefine, BaseService, PageReq, PermissionException, ValidateException } from "../../../index.js";
import { addonRegistry, newAddon } from "../api/index.js";
import { AddonEntity } from "../entity/addon.js";
import { http, logger, utils } from "@certd/basic";
/**
* Addon
*/
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
export class AddonService extends BaseService<AddonEntity> {
@InjectEntityModel(AddonEntity)
repository: Repository<AddonEntity>;
//@ts-ignore
getRepository() {
return this.repository;
}
async page(pageReq: PageReq<AddonEntity>) {
const res = await super.page(pageReq);
res.records = res.records.map(item => {
return item;
});
return res;
}
async add(param) {
let oldEntity = null;
if (param._copyFrom){
oldEntity = await this.info(param._copyFrom);
if (oldEntity == null) {
throw new ValidateException('该Addon配置不存在,请确认是否已被删除');
}
if (oldEntity.userId !== param.userId) {
throw new ValidateException('您无权查看该Addon配置');
}
}
if (!param.userId){
param.isSystem = true
}else{
param.isSystem = false
}
delete param._copyFrom
return await super.add(param);
}
/**
* 修改
* @param param 数据
*/
async update(param) {
const oldEntity = await this.info(param.id);
if (oldEntity == null) {
throw new ValidateException('该Addon配置不存在,请确认是否已被删除');
}
return await super.update(param);
}
async getSimpleInfo(id: number) {
const entity = await this.info(id);
if (entity == null) {
throw new ValidateException('该Addon配置不存在,请确认是否已被删除');
}
return {
id: entity.id,
name: entity.name,
userId: entity.userId,
addonType: entity.addonType,
type: entity.type,
};
}
async getAddonById(id: any, checkUserId: boolean, userId?: number): Promise<any> {
const ctx = {
http: http,
logger: logger,
utils: utils,
};
if (!id){
//使用图片验证码
return await newAddon("captcha", "image", {},ctx);
}
const entity = await this.info(id);
if (entity == null) {
//使用图片验证码
return await newAddon("captcha", "image", {},ctx);
}
if (checkUserId) {
if (userId == null) {
throw new ValidateException('userId不能为空');
}
if (userId !== entity.userId) {
throw new PermissionException('您对该Addon无访问权限');
}
}
const setting = JSON.parse(entity.setting ??"{}")
const input = {
id: entity.id,
...setting,
};
return await newAddon(entity.addonType, entity.type, input,ctx);
}
async getById(id: any, userId: number): Promise<any> {
return await this.getAddonById(id, true, userId);
}
getDefineList(addonType: string) {
return addonRegistry.getDefineList();
}
getDefineByType(type: string,prefix?: string) {
return addonRegistry.getDefine(type,prefix) as AddonDefine;
}
async getSimpleByIds(ids: number[], userId: any) {
if (ids.length === 0) {
return [];
}
if (!userId) {
return [];
}
return await this.repository.find({
where: {
id: In(ids),
userId,
},
select: {
id: true,
name: true,
addonType: true,
type: true,
userId:true,
isSystem: true,
},
});
}
async getDefault(userId: number,addonType: string): Promise<any> {
const res = await this.repository.findOne({
where: {
userId,
addonType
},
order: {
isDefault: 'DESC',
},
});
if (!res) {
return null;
}
return this.buildAddonInstanceConfig(res);
}
private buildAddonInstanceConfig(res: AddonEntity) {
const setting = JSON.parse(res.setting);
return {
id: res.id,
addonType: res.addonType,
type: res.type,
name: res.name,
userId: res.userId,
setting,
};
}
async setDefault(id: number, userId: number,addonType:string) {
if (!id) {
throw new ValidateException('id不能为空');
}
if (!userId) {
throw new ValidateException('userId不能为空');
}
await this.repository.update(
{
userId,
addonType
},
{
isDefault: false,
}
);
await this.repository.update(
{
id,
userId,
addonType
},
{
isDefault: true,
}
);
}
async getOrCreateDefault(opts:{addonType:string,type:string, inputs: any, userId: any}) {
const {addonType,type,inputs,userId} = opts;
const addonDefine = this.getDefineByType( type,addonType)
const defaultConfig = await this.getDefault(userId,addonType);
if (defaultConfig) {
return defaultConfig;
}
const setting = {
...inputs,
};
const res = await this.repository.save({
userId,
addonType,
type: type,
name: addonDefine.title,
setting: JSON.stringify(setting),
isDefault: true,
});
return this.buildAddonInstanceConfig(res);
}
}

View File

@@ -0,0 +1,17 @@
import { IAccessService } from '@certd/pipeline';
import { AddonService } from './addon-service.js';
export class AddonSysGetter implements IAccessService {
addonService: AddonService;
constructor(addonService: AddonService) {
this.addonService = addonService;
}
async getById<T = any>(id: any) {
return await this.addonService.getById(id, 0);
}
async getCommonById<T = any>(id: any) {
return await this.addonService.getById(id, 0);
}
}

View File

@@ -1 +1,2 @@
export * from './access/index.js'; export * from './access/index.js';
export * from './addon/index.js';

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/midway-flyway-js **Note:** Version bump only for package @certd/midway-flyway-js

View File

@@ -1,6 +1,6 @@
{ {
"name": "@certd/midway-flyway-js", "name": "@certd/midway-flyway-js",
"version": "1.36.16", "version": "1.36.19",
"description": "midway with flyway, sql upgrade way ", "description": "midway with flyway, sql upgrade way ",
"private": false, "private": false,
"type": "module", "type": "module",
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11", "typeorm": "^0.3.11",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00" "gitHead": "6d8981479517b5de9634e242c1ebf22e70527ec4"
} }

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Performance Improvements
* 支持ssl.com证书颁发机构 ([27b6dfa](https://github.com/certd/certd/commit/27b6dfa4d2ab3bddd284c3a34511a72e1a513a4c))
* 子域名托管说明 ([39a0223](https://github.com/certd/certd/commit/39a02235cf4416bb5bd1acd3831241efeaa2f602))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Performance Improvements
* 支持p7b证书格式 ([d9f4a57](https://github.com/certd/certd/commit/d9f4a5793d68a017a5d80ad5385cbda603c4e165))
* openapi返回证书时挑选匹配范围最小的那一个增加format参数增加返回值p7b格式增加detail返回 ([2085bcc](https://github.com/certd/certd/commit/2085bcceb61c3723c9bdfec4c4cc0917631ff5e5))
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
### Performance Improvements
* 证书申请任务默认不发送申请成功通知 ([0283bd2](https://github.com/certd/certd/commit/0283bd2f978dbcd13d361129135e439dd9fbc180))
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Performance Improvements ### Performance Improvements

View File

@@ -1,7 +1,7 @@
{ {
"name": "@certd/plugin-cert", "name": "@certd/plugin-cert",
"private": false, "private": false,
"version": "1.36.16", "version": "1.36.19",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -16,10 +16,10 @@
"pub": "npm publish" "pub": "npm publish"
}, },
"dependencies": { "dependencies": {
"@certd/acme-client": "^1.36.16", "@certd/acme-client": "^1.36.19",
"@certd/basic": "^1.36.16", "@certd/basic": "^1.36.19",
"@certd/pipeline": "^1.36.16", "@certd/pipeline": "^1.36.19",
"@certd/plugin-lib": "^1.36.16", "@certd/plugin-lib": "^1.36.19",
"@google-cloud/publicca": "^1.3.0", "@google-cloud/publicca": "^1.3.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
@@ -43,5 +43,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00" "gitHead": "6d8981479517b5de9634e242c1ebf22e70527ec4"
} }

View File

@@ -12,7 +12,7 @@ export class EabAccess extends BaseAccess {
component: { component: {
placeholder: "kid / keyId", placeholder: "kid / keyId",
}, },
helper: "EAB KID google的叫 keyId", helper: "EAB KID google的叫 keyIdssl.com的叫Account/ACME Key",
required: true, required: true,
encrypt: true, encrypt: true,
}) })

View File

@@ -48,8 +48,9 @@ export type CertInfo = {
der?: string; der?: string;
jks?: string; jks?: string;
one?: string; one?: string;
p7b?: string;
}; };
export type SSLProvider = "letsencrypt" | "google" | "zerossl"; export type SSLProvider = "letsencrypt" | "google" | "zerossl" | "sslcom";
export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521"; export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521";
type AcmeServiceOptions = { type AcmeServiceOptions = {
userContext: IContext; userContext: IContext;
@@ -328,8 +329,9 @@ export class AcmeService {
isTest?: boolean; isTest?: boolean;
privateKeyType?: string; privateKeyType?: string;
profile?: string; profile?: string;
preferredChain?: string;
}): Promise<CertInfo> { }): Promise<CertInfo> {
const { email, isTest, csrInfo, dnsProvider, domainsVerifyPlan, profile } = options; const { email, isTest, csrInfo, dnsProvider, domainsVerifyPlan, profile, preferredChain } = options;
const client: acme.Client = await this.getAcmeClient(email, isTest); const client: acme.Client = await this.getAcmeClient(email, isTest);
let domains = options.domains; let domains = options.domains;
@@ -372,6 +374,7 @@ export class AcmeService {
commonName, commonName,
...csrInfo, ...csrInfo,
altNames, altNames,
emailAddress: email,
}, },
privateKey privateKey
); );
@@ -402,6 +405,7 @@ export class AcmeService {
}, },
signal: this.options.signal, signal: this.options.signal,
profile, profile,
preferredChain,
}); });
const crtString = crt.toString(); const crtString = crt.toString();

View File

@@ -28,7 +28,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
"2、子域名被通配符包含的不要填写例如www.foo.com已经被*.foo.com包含不要填写www.foo.com\n" + "2、子域名被通配符包含的不要填写例如www.foo.com已经被*.foo.com包含不要填写www.foo.com\n" +
"3、泛域名只能通配*号那一级(*.foo.com的证书不能用于xxx.yyy.foo.com、不能用于foo.com\n" + "3、泛域名只能通配*号那一级(*.foo.com的证书不能用于xxx.yyy.foo.com、不能用于foo.com\n" +
"4、输入一个空格之后再输入下一个 \n" + "4、输入一个空格之后再输入下一个 \n" +
"5、如果您配置了子域托管解析,请先[设置托管子域名](#/certd/pipeline/subDomain)", "5、如果置了子域托管解析比如免费的二级域名托管在CF或者阿里云,请先[设置托管子域名](#/certd/pipeline/subDomain)",
}) })
domains!: string[]; domains!: string[];
@@ -125,6 +125,10 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
cert.jks = res.jks; cert.jks = res.jks;
} }
if (cert.p7b == null && res.p7b) {
cert.p7b = res.p7b;
}
this.logger.info("转换证书格式成功"); this.logger.info("转换证书格式成功");
} catch (e) { } catch (e) {
this.logger.error("转换证书格式失败", e); this.logger.error("转换证书格式失败", e);
@@ -150,6 +154,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
zip.file("intermediate.crt", cert.ic); zip.file("intermediate.crt", cert.ic);
zip.file("origin.crt", cert.oc); zip.file("origin.crt", cert.oc);
zip.file("one.pem", cert.one); zip.file("one.pem", cert.one);
zip.file("cert.p7b", cert.p7b);
if (cert.pfx) { if (cert.pfx) {
zip.file("cert.pfx", Buffer.from(cert.pfx, "base64")); zip.file("cert.pfx", Buffer.from(cert.pfx, "base64"));
} }

View File

@@ -33,7 +33,7 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
@TaskInput({ @TaskInput({
title: "证书申请成功通知", title: "证书申请成功通知",
value: true, value: false,
component: { component: {
name: "a-switch", name: "a-switch",
vModel: "checked", vModel: "checked",
@@ -41,7 +41,7 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
order: 100, order: 100,
helper: "证书申请成功后是否发送通知,优先使用默认通知渠道", helper: "证书申请成功后是否发送通知,优先使用默认通知渠道",
}) })
successNotify = true; successNotify = false;
// @TaskInput({ // @TaskInput({
// title: "CsrInfo", // title: "CsrInfo",

View File

@@ -17,9 +17,19 @@ export type CertReaderHandleContext = {
tmpIcPath?: string; tmpIcPath?: string;
tmpJksPath?: string; tmpJksPath?: string;
tmpOnePath?: string; tmpOnePath?: string;
tmpP7bPath?: string;
}; };
export type CertReaderHandle = (ctx: CertReaderHandleContext) => Promise<void>; export type CertReaderHandle = (ctx: CertReaderHandleContext) => Promise<void>;
export type HandleOpts = { logger: ILogger; handle: CertReaderHandle }; export type HandleOpts = { logger: ILogger; handle: CertReaderHandle };
const formats = {
pem: ["crt", "key", "ic"],
one: ["one"],
pfx: ["pfx"],
der: ["der"],
jks: ["jks"],
p7b: ["p7b", "key"],
};
export class CertReader { export class CertReader {
cert: CertInfo; cert: CertInfo;
@@ -73,8 +83,17 @@ export class CertReader {
return arr[0] + endStr; return arr[0] + endStr;
} }
toCertInfo(): CertInfo { toCertInfo(format?: string): CertInfo {
return this.cert; if (!format) {
return this.cert;
}
const formatArr = formats[format];
const res: any = {};
formatArr.forEach((key: string) => {
res[key] = this.cert[key];
});
return res;
} }
getCrtDetail(crt: string = this.cert.crt) { getCrtDetail(crt: string = this.cert.crt) {
@@ -124,7 +143,7 @@ export class CertReader {
return domain; return domain;
} }
saveToFile(type: "crt" | "key" | "pfx" | "der" | "oc" | "one" | "ic" | "jks", filepath?: string) { saveToFile(type: "crt" | "key" | "pfx" | "der" | "oc" | "one" | "ic" | "jks" | "p7b", filepath?: string) {
if (!this.cert[type]) { if (!this.cert[type]) {
return; return;
} }
@@ -138,7 +157,7 @@ export class CertReader {
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
} }
if (type === "crt" || type === "key" || type === "ic" || type === "oc" || type === "one") { if (type === "crt" || type === "key" || type === "ic" || type === "oc" || type === "one" || type === "p7b") {
fs.writeFileSync(filepath, this.cert[type]); fs.writeFileSync(filepath, this.cert[type]);
} else { } else {
fs.writeFileSync(filepath, Buffer.from(this.cert[type], "base64")); fs.writeFileSync(filepath, Buffer.from(this.cert[type], "base64"));
@@ -157,17 +176,19 @@ export class CertReader {
const tmpDerPath = this.saveToFile("der"); const tmpDerPath = this.saveToFile("der");
const tmpJksPath = this.saveToFile("jks"); const tmpJksPath = this.saveToFile("jks");
const tmpOnePath = this.saveToFile("one"); const tmpOnePath = this.saveToFile("one");
const tmpP7bPath = this.saveToFile("p7b");
logger.info("本地文件写入成功"); logger.info("本地文件写入成功");
try { try {
return await opts.handle({ return await opts.handle({
reader: this, reader: this,
tmpCrtPath: tmpCrtPath, tmpCrtPath,
tmpKeyPath: tmpKeyPath, tmpKeyPath,
tmpPfxPath: tmpPfxPath, tmpPfxPath,
tmpDerPath: tmpDerPath, tmpDerPath,
tmpIcPath: tmpIcPath, tmpIcPath,
tmpJksPath: tmpJksPath, tmpJksPath,
tmpOcPath: tmpOcPath, tmpOcPath,
tmpP7bPath,
tmpOnePath, tmpOnePath,
}); });
} catch (err) { } catch (err) {
@@ -189,6 +210,7 @@ export class CertReader {
removeFile(tmpIcPath); removeFile(tmpIcPath);
removeFile(tmpJksPath); removeFile(tmpJksPath);
removeFile(tmpOnePath); removeFile(tmpOnePath);
removeFile(tmpP7bPath);
} }
} }
@@ -199,10 +221,10 @@ export class CertReader {
return `${prefix}_${domain}_${timeStr}.${suffix}`; return `${prefix}_${domain}_${timeStr}.${suffix}`;
} }
buildCertName() { buildCertName(prefix: string = "") {
let domain = this.getMainDomain(); let domain = this.getMainDomain();
domain = domain.replaceAll(".", "_").replaceAll("*", "_"); domain = domain.replaceAll(".", "_").replaceAll("*", "_");
return `${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`; return `${prefix}_${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`;
} }
static appendTimeSuffix(name?: string) { static appendTimeSuffix(name?: string) {

View File

@@ -18,11 +18,13 @@ export class CertConverter {
pfx: string; pfx: string;
der: string; der: string;
jks: string; jks: string;
p7b: string;
}> { }> {
const certReader = new CertReader(opts.cert); const certReader = new CertReader(opts.cert);
let pfx: string; let pfx: string;
let der: string; let der: string;
let jks: string; let jks: string;
let p7b: string;
const handle = async (ctx: CertReaderHandleContext) => { const handle = async (ctx: CertReaderHandleContext) => {
// 调用openssl 转pfx // 调用openssl 转pfx
pfx = await this.convertPfx(ctx, opts.pfxPassword, opts.pfxArgs); pfx = await this.convertPfx(ctx, opts.pfxPassword, opts.pfxArgs);
@@ -31,6 +33,8 @@ export class CertConverter {
der = await this.convertDer(ctx); der = await this.convertDer(ctx);
jks = await this.convertJks(ctx, opts.pfxPassword); jks = await this.convertJks(ctx, opts.pfxPassword);
p7b = await this.convertP7b(ctx);
}; };
await certReader.readCertFile({ logger: this.logger, handle }); await certReader.readCertFile({ logger: this.logger, handle });
@@ -39,6 +43,7 @@ export class CertConverter {
pfx, pfx,
der, der,
jks, jks,
p7b,
}; };
} }
@@ -95,6 +100,23 @@ export class CertConverter {
return derCert; return derCert;
} }
async convertP7b(opts: CertReaderHandleContext) {
const { tmpCrtPath } = opts;
const p7bPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + `_cert.p7b`);
const dir = path.dirname(p7bPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
//openssl crl2pkcs7 -nocrl \
// -certfile your_domain.crt \
// -certfile intermediate.crt \
// -out chain.p7b
await this.exec(`openssl crl2pkcs7 -nocrl -certfile ${tmpCrtPath} -out ${p7bPath}`);
const fileBuffer = fs.readFileSync(p7bPath);
const p7bCert = fileBuffer.toString();
fs.unlinkSync(p7bPath);
return p7bCert;
}
async convertJks(opts: CertReaderHandleContext, pfxPassword = "") { async convertJks(opts: CertReaderHandleContext, pfxPassword = "") {
const jksPassword = pfxPassword || "123456"; const jksPassword = pfxPassword || "123456";
try { try {
@@ -113,9 +135,7 @@ export class CertConverter {
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
} }
await this.exec( await this.exec(`keytool -importkeystore -srckeystore ${p12Path} -srcstoretype PKCS12 -srcstorepass "${jksPassword}" -destkeystore ${jksPath} -deststoretype PKCS12 -deststorepass "${jksPassword}" `);
`keytool -importkeystore -srckeystore ${p12Path} -srcstoretype PKCS12 -srcstorepass "${jksPassword}" -destkeystore ${jksPath} -deststoretype PKCS12 -deststorepass "${jksPassword}" `
);
fs.unlinkSync(p12Path); fs.unlinkSync(p12Path);
const fileBuffer = fs.readFileSync(jksPath); const fileBuffer = fs.readFileSync(jksPath);

View File

@@ -89,6 +89,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
{ value: "letsencrypt", label: "Let's Encrypt", icon: "simple-icons:letsencrypt" }, { value: "letsencrypt", label: "Let's Encrypt", icon: "simple-icons:letsencrypt" },
{ value: "google", label: "Google", icon: "flat-color-icons:google" }, { value: "google", label: "Google", icon: "flat-color-icons:google" },
{ value: "zerossl", label: "ZeroSSL", icon: "emojione:digit-zero" }, { value: "zerossl", label: "ZeroSSL", icon: "emojione:digit-zero" },
{ value: "sslcom", label: "SSL.com仅主域名和www免费", icon: "la:expeditedssl" },
], ],
}, },
helper: "Let's Encrypt申请最简单\nGoogle大厂光环兼容性好仅首次需要翻墙获取EAB授权\nZeroSSL需要EAB授权无需翻墙", helper: "Let's Encrypt申请最简单\nGoogle大厂光环兼容性好仅首次需要翻墙获取EAB授权\nZeroSSL需要EAB授权无需翻墙",
@@ -194,6 +195,13 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
}) })
zerosslCommonEabAccessId!: number; zerosslCommonEabAccessId!: number;
@TaskInput({
title: "SSL.com公共EAB授权",
isSys: true,
show: false,
})
sslcomCommonEabAccessId!: number;
@TaskInput({ @TaskInput({
title: "EAB授权", title: "EAB授权",
component: { component: {
@@ -203,11 +211,16 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
maybeNeed: true, maybeNeed: true,
required: false, required: false,
helper: helper:
"需要提供EAB授权\nZeroSSL请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials'\n Google:请查看[google获取eab帮助文档](https://certd.docmirror.cn/guide/use/google/)用过一次后会绑定邮箱后续复用EAB要用同一个邮箱", "需要提供EAB授权" +
"\nZeroSSL请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials'" +
"\nGoogle:请查看[google获取eab帮助文档](https://certd.docmirror.cn/guide/use/google/)用过一次后会绑定邮箱后续复用EAB要用同一个邮箱" +
"\nSSL.com:[SSL.com账号页面](https://secure.ssl.com/account),然后点击api credentials链接然后点击编辑按钮查看Secret key和HMAC key",
mergeScript: ` mergeScript: `
return { return {
show: ctx.compute(({form})=>{ show: ctx.compute(({form})=>{
return (form.sslProvider === 'zerossl' && !form.zerosslCommonEabAccessId) || (form.sslProvider === 'google' && !form.googleCommonEabAccessId) return (form.sslProvider === 'zerossl' && !form.zerosslCommonEabAccessId)
|| (form.sslProvider === 'google' && !form.googleCommonEabAccessId)
|| (form.sslProvider === 'sslcom' && !form.sslcomCommonEabAccessId)
}) })
} }
`, `,
@@ -279,6 +292,29 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
}) })
certProfile!: string; certProfile!: string;
@TaskInput({
title: "首选链",
value: "ISRG Root X1",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "ISRG Root X1", label: "ISRG Root X1" },
{ value: "ISRG Root X2", label: "ISRG Root X2" },
],
},
helper: "仅 Let's Encrypt 可选,默认为 ISRG Root X1",
required: false,
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.sslProvider === 'letsencrypt'
})
}
`,
})
preferredChain!: string;
@TaskInput({ @TaskInput({
title: "使用代理", title: "使用代理",
value: false, value: false,
@@ -339,8 +375,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
async onInit() { async onInit() {
let eab: EabAccess = null; let eab: EabAccess = null;
if (this.sslProvider === "google") { if (this.sslProvider !== "letsencrypt") {
if (this.googleAccessId) { if (this.sslProvider === "google" && this.googleAccessId) {
this.logger.info("当前正在使用 google服务账号授权获取EAB"); this.logger.info("当前正在使用 google服务账号授权获取EAB");
const googleAccess = await this.getAccess(this.googleAccessId); const googleAccess = await this.getAccess(this.googleAccessId);
const googleClient = new GoogleClient({ const googleClient = new GoogleClient({
@@ -348,24 +384,19 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
logger: this.logger, logger: this.logger,
}); });
eab = await googleClient.getEab(); eab = await googleClient.getEab();
} else if (this.eabAccessId) {
this.logger.info("当前正在使用 google EAB授权");
eab = await this.getAccess(this.eabAccessId);
} else if (this.googleCommonEabAccessId) {
this.logger.info("当前正在使用 google 公共EAB授权");
eab = await this.getAccess(this.googleCommonEabAccessId, true);
} else { } else {
throw new Error("google需要配置EAB授权或服务账号授权"); const getEab = async (type: string) => {
} if (this.eabAccessId) {
} else if (this.sslProvider === "zerossl") { this.logger.info(`当前正在使用 ${type} EAB授权`);
if (this.eabAccessId) { eab = await this.getAccess(this.eabAccessId);
this.logger.info("当前正在使用 zerossl EAB授权"); } else if (this[`${type}CommonEabAccessId`]) {
eab = await this.getAccess(this.eabAccessId); this.logger.info(`当前正在使用 ${type} 公共EAB授权`);
} else if (this.zerosslCommonEabAccessId) { eab = await this.getAccess(this[`${type}CommonEabAccessId`], true);
this.logger.info("当前正在使用 zerossl 公共EAB授权"); } else {
eab = await this.getAccess(this.zerosslCommonEabAccessId, true); throw new Error(`${type}需要配置EAB授权`);
} else { }
throw new Error("zerossl需要配置EAB授权"); };
await getEab(this.sslProvider);
} }
} }
this.eab = eab; this.eab = eab;
@@ -397,12 +428,12 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
const csrInfo = _.merge( const csrInfo = _.merge(
{ {
country: "CN", // country: "CN",
state: "GuangDong", // state: "GuangDong",
locality: "ShengZhen", // locality: "ShengZhen",
organization: "CertD Org.", // organization: "CertD Org.",
organizationUnit: "IT Department", // organizationUnit: "IT Department",
emailAddress: email, // emailAddress: email,
}, },
this.csrInfo ? JSON.parse(this.csrInfo) : {} this.csrInfo ? JSON.parse(this.csrInfo) : {}
); );
@@ -430,6 +461,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
isTest: false, isTest: false,
privateKeyType: this.privateKeyType, privateKeyType: this.privateKeyType,
profile: this.certProfile, profile: this.certProfile,
preferredChain: this.preferredChain,
}); });
const certInfo = this.formatCerts(cert); const certInfo = this.formatCerts(cert);

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Performance Improvements
* ssh 增加超时断开连接默认10分钟超时 ([c24a040](https://github.com/certd/certd/commit/c24a040c19cacafc79228d7a7649af93837d94a1))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Performance Improvements
* 腾讯云插件支持国际版 ([58e82d5](https://github.com/certd/certd/commit/58e82d5dbd4ebf089ef239578ef9b68454d17b30))
* 支持部署到阿里云云原生API网关、AI网关 ([2ca20be](https://github.com/certd/certd/commit/2ca20be197720201fceabcce9d927f4dbc1cc872))
* ssh 配置sudo免密提示 ([e1e7011](https://github.com/certd/certd/commit/e1e7011853ad0c5bd7b09c3690861d5aa34b2db4))
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
### Performance Improvements
* 腾讯云关闭证书通知增加开关选项,在腾讯云授权里面 ([a77c777](https://github.com/certd/certd/commit/a77c777980dd38d97d983124eeed1596879bba95))
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/plugin-lib **Note:** Version bump only for package @certd/plugin-lib

View File

@@ -1,7 +1,7 @@
{ {
"name": "@certd/plugin-lib", "name": "@certd/plugin-lib",
"private": false, "private": false,
"version": "1.36.16", "version": "1.36.19",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -21,8 +21,8 @@
"@alicloud/pop-core": "^1.7.10", "@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.10", "@alicloud/tea-util": "^1.4.10",
"@aws-sdk/client-s3": "^3.787.0", "@aws-sdk/client-s3": "^3.787.0",
"@certd/basic": "^1.36.16", "@certd/basic": "^1.36.19",
"@certd/pipeline": "^1.36.16", "@certd/pipeline": "^1.36.19",
"@kubernetes/client-node": "0.21.0", "@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0", "ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5", "basic-ftp": "^5.0.5",
@@ -53,5 +53,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00" "gitHead": "6d8981479517b5de9634e242c1ebf22e70527ec4"
} }

View File

@@ -7,9 +7,9 @@ export type AliyunClientV2Req = {
// 接口 HTTP 方法 // 接口 HTTP 方法
method?: "GET" | "POST"; method?: "GET" | "POST";
authType?: "AK"; authType?: "AK";
style?: "RPC"; style?: "RPC" | "ROA";
// 接口 PATH // 接口 PATH
pathname?: `/`; pathname?: string;
data?: any; data?: any;
}; };
@@ -63,10 +63,10 @@ export class AliyunClientV2 {
protocol: "HTTPS", protocol: "HTTPS",
// 接口 HTTP 方法 // 接口 HTTP 方法
method: req.method ?? "POST", method: req.method ?? "POST",
authType: "AK", authType: req.authType ?? "AK",
style: "RPC", style: req.style ?? "RPC",
// 接口 PATH // 接口 PATH
pathname: `/`, pathname: req.pathname ?? `/`,
// 接口请求体内容格式 // 接口请求体内容格式
reqBodyType: "json", reqBodyType: "json",
// 接口响应体内容格式 // 接口响应体内容格式

View File

@@ -9,7 +9,8 @@ export type AliyunCertInfo = {
export type AliyunSslClientOpts = { export type AliyunSslClientOpts = {
access: AliyunAccess; access: AliyunAccess;
logger: ILogger; logger: ILogger;
endpoint: string; endpoint?: string;
region?: string;
}; };
export type AliyunSslGetResourceListReq = { export type AliyunSslGetResourceListReq = {
@@ -48,10 +49,19 @@ export class AliyunSslClient {
async getClient() { async getClient() {
const access = this.opts.access; const access = this.opts.access;
const client = new AliyunClient({ logger: this.opts.logger }); const client = new AliyunClient({ logger: this.opts.logger });
let endpoint = this.opts.endpoint || "cas.aliyuncs.com";
if (this.opts.endpoint == null && this.opts.region) {
if (this.opts.region === "cn-hangzhou") {
endpoint = "cas.aliyuncs.com";
} else {
endpoint = `cas.${this.opts.region}.aliyuncs.com`;
}
}
await client.init({ await client.init({
accessKeyId: access.accessKeyId, accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret, accessKeySecret: access.accessKeySecret,
endpoint: `https://${this.opts.endpoint || "cas.aliyuncs.com"}`, endpoint: `https://${endpoint}`,
apiVersion: "2020-04-07", apiVersion: "2020-04-07",
}); });
return client; return client;

View File

@@ -5,6 +5,7 @@ import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
title: "天翼云授权", title: "天翼云授权",
desc: "", desc: "",
icon: "ant-design:aliyun-outlined", icon: "ant-design:aliyun-outlined",
order: 2,
}) })
export class CtyunAccess extends BaseAccess { export class CtyunAccess extends BaseAccess {
@AccessInput({ @AccessInput({

View File

@@ -6,6 +6,7 @@ import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
desc: "", desc: "",
icon: "svg:icon-qiniuyun", icon: "svg:icon-qiniuyun",
input: {}, input: {},
order: 2,
}) })
export class QiniuAccess extends BaseAccess { export class QiniuAccess extends BaseAccess {
@AccessInput({ @AccessInput({

View File

@@ -5,6 +5,7 @@ import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
desc: "", desc: "",
icon: "clarity:host-line", icon: "clarity:host-line",
input: {}, input: {},
order: 0,
}) })
export class SshAccess extends BaseAccess { export class SshAccess extends BaseAccess {
@AccessInput({ @AccessInput({
@@ -63,6 +64,22 @@ export class SshAccess extends BaseAccess {
}) })
passphrase!: string; passphrase!: string;
@AccessInput({
title: "脚本类型",
helper: "bash 、sh 、fish",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "default", label: "默认" },
{ value: "sh", label: "sh" },
{ value: "bash", label: "bash" },
{ value: "fish", label: "fish(不支持set -e)" },
],
},
})
scriptType: string;
@AccessInput({ @AccessInput({
title: "伪终端", title: "伪终端",
helper: "如果登录报错all authentication methods failed可以尝试开启伪终端模式进行keyboard-interactive方式登录\n开启后对日志输出有一定的影响", helper: "如果登录报错all authentication methods failed可以尝试开启伪终端模式进行keyboard-interactive方式登录\n开启后对日志输出有一定的影响",
@@ -85,6 +102,15 @@ export class SshAccess extends BaseAccess {
}) })
socksProxy!: string; socksProxy!: string;
@AccessInput({
title: "超时时间",
helper: "执行命令的超时时间,单位秒,默认30分钟",
component: {
name: "a-input-number",
},
})
timeout: number;
@AccessInput({ @AccessInput({
title: "是否Windows", title: "是否Windows",
helper: "如果是Windows主机请勾选此项\n并且需要windows[安装OpenSSH](https://certd.docmirror.cn/guide/use/host/windows.html)", helper: "如果是Windows主机请勾选此项\n并且需要windows[安装OpenSSH](https://certd.docmirror.cn/guide/use/host/windows.html)",
@@ -135,9 +161,10 @@ export class SshAccess extends BaseAccess {
const { SshClient } = await import("./ssh.js"); const { SshClient } = await import("./ssh.js");
const client = new SshClient(this.ctx.logger); const client = new SshClient(this.ctx.logger);
const script = ["echo hello", "exit"];
await client.exec({ await client.exec({
connectConf: this, connectConf: this,
script: "echo hello", script: script,
}); });
return "ok"; return "ok";
} }

View File

@@ -247,6 +247,9 @@ export class AsyncSsh2Client {
const err = this.convert(iconv, ret); const err = this.convert(iconv, ret);
stdErr += err; stdErr += err;
hasErrorLog = true; hasErrorLog = true;
if (err.includes("sudo: a password is required")) {
this.logger.warn("请配置sudo免密否则命令无法执行");
}
this.logger.error(`[${this.connConf.host}][error]: ` + err.trimEnd()); this.logger.error(`[${this.connConf.host}][error]: ` + err.trimEnd());
}); });
}); });
@@ -466,7 +469,8 @@ export class SshClient {
async isCmd(conn: AsyncSsh2Client) { async isCmd(conn: AsyncSsh2Client) {
const spec = await conn.exec("echo %COMSPEC% "); const spec = await conn.exec("echo %COMSPEC% ");
if (spec.toString().trim() === "%COMSPEC%") { const ret = spec.toString().trim();
if (ret.includes("%COMSPEC%") && !ret.includes("echo %COMSPEC%")) {
return false; return false;
} else { } else {
return true; return true;
@@ -539,8 +543,16 @@ export class SshClient {
} }
} }
if (isLinux && options.stopOnError !== false) { if (isLinux) {
script = "set -e\n" + script; if (options.connectConf.scriptType == "bash") {
script = "#!/usr/bin/env bash \n" + script;
} else if (options.connectConf.scriptType == "sh") {
script = "#!/bin/sh\n" + script;
}
if (options.connectConf.scriptType != "fish" && options.stopOnError !== false) {
script = "set -e\n" + script;
}
} }
return await conn.exec(script as string, { throwOnStdErr }); return await conn.exec(script as string, { throwOnStdErr });
@@ -584,10 +596,15 @@ export class SshClient {
} }
throw e; throw e;
} }
let timeoutId = null;
try { try {
timeoutId = setTimeout(() => {
this.logger.info("执行超时,断开连接");
conn.end();
}, 1000 * (connectConf.timeout || 1800));
return await callable(conn); return await callable(conn);
} finally { } finally {
clearTimeout(timeoutId);
conn.end(); conn.end();
} }
} }

View File

@@ -4,6 +4,7 @@ import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
name: "tencent", name: "tencent",
title: "腾讯云", title: "腾讯云",
icon: "svg:icon-tencentcloud", icon: "svg:icon-tencentcloud",
order: 0,
}) })
export class TencentAccess extends BaseAccess { export class TencentAccess extends BaseAccess {
@AccessInput({ @AccessInput({
@@ -46,7 +47,21 @@ export class TencentAccess extends BaseAccess {
}) })
accountType: string; accountType: string;
@AccessInput({
title: "关闭证书过期通知",
value: true,
component: {
name: "a-switch",
vModel: "checked",
},
})
closeExpiresNotify: boolean = true;
isIntl() { isIntl() {
return this.accountType === "intl"; return this.accountType === "intl";
} }
intlDomain() {
return this.isIntl() ? "intl." : "";
}
} }

View File

@@ -26,7 +26,7 @@ export class TencentSslClient {
region: this.region, region: this.region,
profile: { profile: {
httpProfile: { httpProfile: {
endpoint: "ssl.tencentcloudapi.com", endpoint: this.access.isIntl() ? "ssl.intl.tencentcloudapi.com" : "ssl.tencentcloudapi.com",
}, },
}, },
}; };
@@ -50,7 +50,10 @@ export class TencentSslClient {
const ret = await client.UploadCertificate(params); const ret = await client.UploadCertificate(params);
this.checkRet(ret); this.checkRet(ret);
this.logger.info(`证书[${opts.certName}]上传成功tencentCertId=`, ret.CertificateId); this.logger.info(`证书[${opts.certName}]上传成功tencentCertId=`, ret.CertificateId);
await this.switchCertNotify([ret.CertificateId], true); if (this.access.closeExpiresNotify) {
await this.switchCertNotify([ret.CertificateId], true);
}
return ret.CertificateId; return ret.CertificateId;
} }
@@ -73,9 +76,26 @@ export class TencentSslClient {
return res; return res;
} }
async DescribeCertificates(params: any) { async DescribeHostUploadUpdateRecordDetail(params: any) {
const client = await this.getSslClient(); const client = await this.getSslClient();
const res = await client.DescribeCertificates(params); const res = await client.request("DescribeHostUploadUpdateRecordDetail", params);
this.checkRet(res);
return res;
}
async UploadUpdateCertificateInstance(params: any) {
const client = await this.getSslClient();
const res = await client.request("UploadUpdateCertificateInstance", params);
this.checkRet(res);
return res;
}
async DescribeCertificates(params: { Limit?: number; Offset?: number; SearchKey?: string }) {
const client = await this.getSslClient();
const res = await client.DescribeCertificates({
ExpirationSort: "ASC",
...params,
});
this.checkRet(res); this.checkRet(res);
return res; return res;
} }

View File

@@ -3,6 +3,39 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Bug Fixes
* 修复批量流水线执行时日志显示错乱的问题 ([4372adc](https://github.com/certd/certd/commit/4372adc703b9a4c785664054ab2a533626d815a8))
* 修复远程数据选择无法过滤的bug ([6cbb073](https://github.com/certd/certd/commit/6cbb0739f8428d51b0712f718fe4d236cc087cf9))
* 修复mysql下购买套餐加量包无效的bug ([c26ad4c](https://github.com/certd/certd/commit/c26ad4c8075f0606d45b8da13915737968d6191a))
### Performance Improvements
* 创建证书时支持选择通知时机 ([0e96bfd](https://github.com/certd/certd/commit/0e96bfdfa377824d204e72923d1176408ae6b300))
* 商业版隐藏文档相关链接 ([4443a1c](https://github.com/certd/certd/commit/4443a1c0308fa6b95a05efd73d15d24b65d641c9))
* 商业版隐藏文档相关链接 ([db89561](https://github.com/certd/certd/commit/db8956148083bc4f988226ccf719940d08158a27))
* 支持根据id更新证书证书Id不变接口不过该接口为白名单功能普通腾讯云账户无法使用 ([fe9c4f3](https://github.com/certd/certd/commit/fe9c4f3391ff07c01dd9a252225f69a129c39050))
* 子域名托管说明 ([39a0223](https://github.com/certd/certd/commit/39a02235cf4416bb5bd1acd3831241efeaa2f602))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Bug Fixes
* 修复cron选择组件星期显示错误的bug ([eb75e52](https://github.com/certd/certd/commit/eb75e52278f94a72643f7317e6740fb42666c68a))
### Performance Improvements
* 短信验证码支持腾讯云 ([9108459](https://github.com/certd/certd/commit/9108459ae42bcd95a59acba164a64e82e5f2cfe6))
* 商业版支持自定义插件的参数配置 ([17f23f3](https://github.com/certd/certd/commit/17f23f37516af925d5049291d67d41e4271f81f8))
* 支持p7b证书格式 ([d9f4a57](https://github.com/certd/certd/commit/d9f4a5793d68a017a5d80ad5385cbda603c4e165))
* openapi返回证书时挑选匹配范围最小的那一个增加format参数增加返回值p7b格式增加detail返回 ([2085bcc](https://github.com/certd/certd/commit/2085bcceb61c3723c9bdfec4c4cc0917631ff5e5))
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/ui-client
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16) ## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Bug Fixes ### Bug Fixes

View File

@@ -23,5 +23,6 @@
</div> </div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
<script src="https://static.geetest.com/v4/gt4.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,6 +1,6 @@
{ {
"name": "@certd/ui-client", "name": "@certd/ui-client",
"version": "1.36.16", "version": "1.36.19",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite --open", "dev": "vite --open",
@@ -30,6 +30,7 @@
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "^7.0.1",
"@aws-sdk/client-s3": "^3.535.0", "@aws-sdk/client-s3": "^3.535.0",
"@aws-sdk/s3-request-presigner": "^3.535.0", "@aws-sdk/s3-request-presigner": "^3.535.0",
"@certd/vue-js-cron-light": "^4.0.14",
"@ctrl/tinycolor": "^4.1.0", "@ctrl/tinycolor": "^4.1.0",
"@fast-crud/fast-crud": "^1.25.13", "@fast-crud/fast-crud": "^1.25.13",
"@fast-crud/fast-extends": "^1.25.13", "@fast-crud/fast-extends": "^1.25.13",
@@ -43,7 +44,6 @@
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tanstack/vue-store": "^0.7.0", "@tanstack/vue-store": "^0.7.0",
"@vee-validate/zod": "^4.15.0", "@vee-validate/zod": "^4.15.0",
"@vue-js-cron/light": "^4.0.5",
"@vue/shared": "^3.5.13", "@vue/shared": "^3.5.13",
"@vueuse/core": "^10.11.0", "@vueuse/core": "^10.11.0",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
@@ -103,8 +103,8 @@
"zod-defaults": "^0.1.3" "zod-defaults": "^0.1.3"
}, },
"devDependencies": { "devDependencies": {
"@certd/lib-iframe": "^1.36.16", "@certd/lib-iframe": "^1.36.19",
"@certd/pipeline": "^1.36.16", "@certd/pipeline": "^1.36.19",
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12", "@types/chai": "^4.3.12",

View File

@@ -0,0 +1,50 @@
<template>
<component :is="captchaComponent" v-if="settingStore.inited" ref="captchaRef" class="captcha_input" :captcha-get="getCaptcha" @change="onChange" />
</template>
<script setup lang="ts">
import { ref, computed, defineAsyncComponent } from "vue";
import { useSettingStore } from "/@/store/settings";
import { nanoid } from "nanoid";
import { request } from "/@/api/service";
const captchaRef = ref(null);
const settingStore = useSettingStore();
const emits = defineEmits(["update:modelValue", "change"]);
const captchaImpls = import.meta.glob("./captchas/*.vue");
const captchaAddonId = computed(() => {
return settingStore.sysPublic.captchaAddonId ?? 0;
});
const captchaComponent = computed(() => {
let type = "image";
if (settingStore.sysPublic.captchaAddonId && settingStore.sysPublic.captchaType) {
type = settingStore.sysPublic.captchaType;
}
const componentName = `${type}_captcha`;
return defineAsyncComponent(captchaImpls[`./captchas/${componentName}.vue`]);
});
async function getCaptcha(): Promise<any> {
const randomStr = nanoid(10);
return await request({
url: `/basic/code/captcha/get?randomStr=${randomStr}`,
method: "post",
data: {
captchaAddonId: captchaAddonId.value,
},
});
}
function onChange(data) {
emits("update:modelValue", data);
emits("change", data);
}
async function getCaptchaForm() {
return await captchaRef.value.getCaptchaForm();
}
defineExpose({
getCaptchaForm,
});
</script>

View File

@@ -0,0 +1,96 @@
<template>
<div ref="captchaRef" class="geetest_captcha_wrapper"></div>
</template>
<script setup lang="ts">
import { onMounted, defineProps, defineEmits, ref, onUnmounted } from "vue";
import { useSettingStore } from "/@/store/settings";
import { request } from "/src/api/service";
import { notification } from "ant-design-vue";
defineOptions({
name: "GeetestCaptcha",
});
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
captchaGet: () => Promise<any>;
}>();
const captchaRef = ref(null);
// const addonApi = createAddonApi();
const settingStore = useSettingStore();
const captchaInstanceRef = ref({});
async function init() {
// if (!initGeetest4) {
// await import("https://static.geetest.com/v4/gt4.js");
// }
const { captchaId } = await props.captchaGet();
// @ts-ignore
initGeetest4(
{
captchaId: captchaId,
},
(captcha: any) => {
// captcha为验证码实例
captcha.appendTo(captchaRef.value); // 调用appendTo将验证码插入到页的某一个元素中这个元素用户可以自定义
captchaInstanceRef.value.instance = captcha;
captchaInstanceRef.value.captchaId = captchaId;
}
);
}
function getCaptchaForm() {
if (!captchaInstanceRef.value?.instance) {
// notification.error({
// message: "验证码还未初始化",
// });
return false;
}
const result = captchaInstanceRef.value.instance.getValidate();
if (!result) {
// notification.error({
// message: "请先完成验证码验证",
// });
return false;
}
result.captcha_id = captchaInstanceRef.value.captchaId;
return result;
}
const valueRef = ref(null);
const timeoutId = setInterval(() => {
const form = getCaptchaForm();
if (form && valueRef.value != form) {
console.log("form", form);
valueRef.value = form;
emitChange(form);
}
}, 1000);
onUnmounted(() => {
clearTimeout(timeoutId);
});
function emitChange(value: string) {
emit("update:modelValue", value);
emit("change", value);
}
defineExpose({
getCaptchaForm,
});
onMounted(async () => {
await init();
});
</script>
<style lang="less">
.geetest_captcha_wrapper {
.geetest_captcha {
.geetest_holder {
width: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,59 @@
<template>
<div class="flex">
<a-input :value="valueRef" placeholder="请输入图片验证码" autocomplete="off" @update:value="onChange">
<template #prefix>
<fs-icon icon="ion:image-outline"></fs-icon>
</template>
</a-input>
<div class="input-right pointer" title="点击刷新">
<img class="image-code" :src="imageCodeSrc" @click="resetImageCode" />
</div>
</div>
</template>
<script setup lang="ts">
import { defineEmits, defineExpose, defineProps, ref } from "vue";
import { nanoid } from "nanoid";
const props = defineProps<{
captchaGet?: () => Promise<any>;
}>();
defineOptions({
name: "ImageCaptcha",
});
const emit = defineEmits(["update:modelValue", "change"]);
const valueRef = ref("");
const randomStrRef = ref();
const imageCodeSrc = ref();
async function resetImageCode() {
const res = await props.captchaGet();
randomStrRef.value = res.randomStr;
valueRef.value = "";
emitChange(null);
imageCodeSrc.value = "data:image/svg+xml," + encodeURIComponent(res.imageData);
}
function getCaptchaForm() {
return {
imageCode: valueRef.value,
randomStr: randomStrRef.value,
};
}
defineExpose({
resetImageCode,
getCaptchaForm,
});
resetImageCode();
function onChange(value: string) {
valueRef.value = value;
const form = getCaptchaForm();
emitChange(form);
}
function emitChange(value) {
emit("update:modelValue", value);
emit("change", value);
}
</script>

View File

@@ -4,8 +4,8 @@ import vip from "./vip-button/install.js";
import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue"; import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue";
import CronEditor from "./cron-editor/index.vue"; import CronEditor from "./cron-editor/index.vue";
import FoldBox from "./fold-box.vue"; import FoldBox from "./fold-box.vue";
import { CronLight } from "@vue-js-cron/light"; import { CronLight } from "@certd/vue-js-cron-light";
import "@vue-js-cron/light/dist/light.css"; import "@certd/vue-js-cron-light/dist/light.css";
import Plugins from "./plugins/index"; import Plugins from "./plugins/index";
import LoadingButton from "./loading-button.vue"; import LoadingButton from "./loading-button.vue";
import IconSelect from "./icon-select.vue"; import IconSelect from "./icon-select.vue";

View File

@@ -66,7 +66,7 @@ const getOptions = async () => {
const input = (pluginType === "plugin" ? form?.input : form) || {}; const input = (pluginType === "plugin" ? form?.input : form) || {};
for (let key in define.input) { for (let key in define.input) {
const inWatches = props.watches.includes(key); const inWatches = props.watches?.includes(key);
const inputDefine = define.input[key]; const inputDefine = define.input[key];
if (inWatches && inputDefine.required) { if (inWatches && inputDefine.required) {
const value = input[key]; const value = input[key];

View File

@@ -105,7 +105,7 @@ const getOptions = async () => {
const input = (pluginType === "plugin" ? form?.input : form) || {}; const input = (pluginType === "plugin" ? form?.input : form) || {};
for (let key in define.input) { for (let key in define.input) {
const inWatches = props.watches.includes(key); const inWatches = props.watches?.includes(key);
const inputDefine = define.input[key]; const inputDefine = define.input[key];
if (inWatches && inputDefine.required) { if (inWatches && inputDefine.required) {
const value = input[key]; const value = input[key];
@@ -169,7 +169,7 @@ const getOptions = async () => {
}; };
const filterOption = (input: string, option: any) => { const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 || String(option.value).toLowerCase().indexOf(input.toLowerCase()); return option.label.toLowerCase().includes(input.toLowerCase()) || String(option.value).toLowerCase().includes(input.toLowerCase());
}; };
async function onClick() { async function onClick() {

View File

@@ -0,0 +1,191 @@
<template>
<div class="remote-select">
<div class="flex flex-row">
<a-tree-select class="remote-tree-select-input" :tree-data="optionsRef" :value="value" tree-checkable allow-clear v-bind="attrs" @click="onClick" @update:value="emit('update:value', $event)"> </a-tree-select>
<div class="ml-5">
<fs-button :loading="loading" title="刷新选项" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
</div>
</div>
<div class="helper" :class="{ error: hasError }">
{{ message }}
</div>
</div>
</template>
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { defineComponent, inject, ref, useAttrs, watch, Ref } from "vue";
import { PluginDefine } from "@certd/pipeline";
defineOptions({
name: "RemoteTreeSelect",
});
const props = defineProps<
{
watches: string[];
search?: boolean;
pager?: boolean;
} & ComponentPropsType
>();
const emit = defineEmits<{
"update:value": any;
}>();
const attrs = useAttrs();
const getCurrentPluginDefine: any = inject("getCurrentPluginDefine", () => {
return {};
});
const getScope: any = inject("get:scope", () => {
return {};
});
const getPluginType: any = inject("get:plugin:type", () => {
return "plugin";
});
const searchKeyRef = ref("");
const optionsRef = ref([]);
const message = ref("");
const hasError = ref(false);
const loading = ref(false);
const pagerRef: Ref = ref({
current: 1,
});
const getOptions = async () => {
if (loading.value) {
return;
}
if (!getCurrentPluginDefine) {
return;
}
const define: PluginDefine = getCurrentPluginDefine()?.value;
if (!define) {
return;
}
const pluginType = getPluginType();
const { form } = getScope();
const input = (pluginType === "plugin" ? form?.input : form) || {};
for (let key in define.input) {
const inWatches = props.watches?.includes(key);
const inputDefine = define.input[key];
if (inWatches && inputDefine.required) {
const value = input[key];
if (value == null || value === "") {
console.log("remote-select required", key);
return;
}
}
}
message.value = "";
hasError.value = false;
loading.value = true;
const pageNo = pagerRef.value.pageNo;
const pageSize = pagerRef.value.pageSize;
try {
const res = await doRequest(
{
type: pluginType,
typeName: form.type,
action: props.action,
input,
data: {
searchKey: props.search ? searchKeyRef.value : "",
pageNo,
pageSize,
},
},
{
onError(err: any) {
hasError.value = true;
message.value = `获取选项出错:${err.message}`;
},
showErrorNotify: false,
}
);
const list = res?.list || res || [];
if (list.length > 0) {
message.value = "获取数据成功,请从下拉框中选择";
} else {
message.value = "获取数据成功,没有数据";
}
optionsRef.value = list;
pagerRef.value.total = list.length;
if (props.pager) {
if (res.pageNo != null) {
pagerRef.value.pageNo = res.pageNo ?? 1;
}
if (res.pageSize != null) {
pagerRef.value.pageSize = res.pageSize ?? 100;
}
if (res.total != null) {
pagerRef.value.total = res.total ?? list.length;
}
}
return res;
} finally {
loading.value = false;
}
};
async function onClick() {
if (optionsRef.value?.length === 0) {
await refreshOptions();
}
}
async function refreshOptions() {
await getOptions();
}
async function doSearch() {
pagerRef.value.pageNo = 1;
await refreshOptions();
}
watch(
() => {
const pluginType = getPluginType();
const { form, key } = getScope();
const input = (pluginType === "plugin" ? form?.input : form) || {};
const watches = {};
for (const key of props.watches) {
//@ts-ignore
watches[key] = input[key];
}
return {
form: watches,
key,
};
},
async (value, oldValue) => {
const { form } = value;
const oldForm: any = oldValue?.form;
let changed = oldForm == null || optionsRef.value.length == 0;
for (const key of props.watches) {
//@ts-ignore
if (oldForm && form[key] != oldForm[key]) {
changed = true;
break;
}
}
if (changed) {
await getOptions();
}
},
{
immediate: true,
}
);
async function onPageChange(current: any) {
await refreshOptions();
}
</script>
<style lang="less"></style>

View File

@@ -2,6 +2,7 @@ import SynologyIdDeviceGetter from "./synology/device-id-getter.vue";
import RemoteAutoComplete from "./common/remote-auto-complete.vue"; import RemoteAutoComplete from "./common/remote-auto-complete.vue";
import RemoteSelect from "./common/remote-select.vue"; import RemoteSelect from "./common/remote-select.vue";
import RemoteInput from "./common/remote-input.vue"; import RemoteInput from "./common/remote-input.vue";
import RemoteTreeSelect from "./common/remote-tree-select.vue";
import CertDomainsGetter from "./common/cert-domains-getter.vue"; import CertDomainsGetter from "./common/cert-domains-getter.vue";
import OutputSelector from "/@/components/plugins/common/output-selector/index.vue"; import OutputSelector from "/@/components/plugins/common/output-selector/index.vue";
import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selector/index.vue"; import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selector/index.vue";
@@ -24,6 +25,7 @@ export default {
app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter); app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter);
app.component("RemoteAutoComplete", RemoteAutoComplete); app.component("RemoteAutoComplete", RemoteAutoComplete);
app.component("RemoteSelect", RemoteSelect); app.component("RemoteSelect", RemoteSelect);
app.component("RemoteTreeSelect", RemoteTreeSelect);
app.component("RemoteInput", RemoteInput); app.component("RemoteInput", RemoteInput);
app.component("CertDomainsGetter", CertDomainsGetter); app.component("CertDomainsGetter", CertDomainsGetter);
app.component("InputPassword", InputPassword); app.component("InputPassword", InputPassword);

View File

@@ -21,6 +21,7 @@ export default {
pipeline: "Pipeline", pipeline: "Pipeline",
domain: "Domain", domain: "Domain",
deployTimes: "Deployments", deployTimes: "Deployments",
monitorCount: "DomainMonitors",
duration: "Duration", duration: "Duration",
price: "Price", price: "Price",
paymentMethod: "Payment Method", paymentMethod: "Payment Method",
@@ -218,6 +219,7 @@ export default {
triggerCronHelper: triggerCronHelper:
"Click the button above to choose a daily execution time.\nIt is recommended to trigger once per day. The task will be skipped if the certificate has not expired and will not be executed repeatedly.", "Click the button above to choose a daily execution time.\nIt is recommended to trigger once per day. The task will be skipped if the certificate has not expired and will not be executed repeatedly.",
notificationTitle: "Failure Notification", notificationTitle: "Failure Notification",
notificationWhen: "Notification Timing",
notificationHelper: "Get real-time alerts when the task fails", notificationHelper: "Get real-time alerts when the task fails",
groupIdTitle: "Pipeline Group", groupIdTitle: "Pipeline Group",
}, },
@@ -564,7 +566,7 @@ export default {
ipv6Priority: "IPv6 Priority", ipv6Priority: "IPv6 Priority",
dualStackNetworkHelper: "If IPv6 priority is selected, enable IPv6 in docker-compose.yaml", dualStackNetworkHelper: "If IPv6 priority is selected, enable IPv6 in docker-compose.yaml",
enableCommonCnameService: "Enable Public CNAME Service", enableCommonCnameService: "Enable Public CNAME Service",
commonCnameHelper: "Allow use of public CNAME service. If disabled and no <router-link to='/sys/cname/provider'>custom CNAME service</router-link> is set, CNAME proxy certificate application will not work.", commonCnameHelper: "Allow use of public CNAME service. If disabled and no <a href='#/sys/cname/provider'>custom CNAME service</a> is set, CNAME proxy certificate application will not work.",
enableCommonSelfServicePasswordRetrieval: "Enable self-service password recovery", enableCommonSelfServicePasswordRetrieval: "Enable self-service password recovery",
saveButton: "Save", saveButton: "Save",
stopSuccess: "Stopped successfully", stopSuccess: "Stopped successfully",
@@ -587,6 +589,7 @@ export default {
commFeature: "Commercial feature", commFeature: "Commercial feature",
smsProvider: "SMS provider", smsProvider: "SMS provider",
aliyunSms: "Aliyun SMS", aliyunSms: "Aliyun SMS",
tencentSms: "Tencent SMS",
yfySms: "YFY SMS", yfySms: "YFY SMS",
smsTest: "SMS test", smsTest: "SMS test",
testMobilePlaceholder: "Enter test mobile number", testMobilePlaceholder: "Enter test mobile number",
@@ -708,6 +711,10 @@ export default {
setting: { setting: {
showRunStrategy: "Show RunStrategy", showRunStrategy: "Show RunStrategy",
showRunStrategyHelper: "Allow modify the run strategy of the task", showRunStrategyHelper: "Allow modify the run strategy of the task",
captchaEnabled: "Enable Login Captcha",
captchaHelper: "Whether to enable captcha verification for login",
captchaType: "Captcha Type",
}, },
}, },
modal: { modal: {
@@ -728,4 +735,8 @@ export default {
challengeSetting: "Challenge Setting", challengeSetting: "Challenge Setting",
gotoCnameTip: "Please go to CNAME Record Page", gotoCnameTip: "Please go to CNAME Record Page",
}, },
addonSelector: {
select: "Select",
placeholder: "select please",
},
}; };

View File

@@ -25,6 +25,7 @@ export default {
pipeline: "流水线", pipeline: "流水线",
domain: "域名", domain: "域名",
deployTimes: "部署次数", deployTimes: "部署次数",
monitorCount: "域名监控数",
duration: "时长", duration: "时长",
price: "价格", price: "价格",
paymentMethod: "支付方式", paymentMethod: "支付方式",
@@ -223,6 +224,7 @@ export default {
triggerCronTitle: "定时触发", triggerCronTitle: "定时触发",
triggerCronHelper: "点击上面的按钮,选择每天几点定时执行。\n建议设置为每天触发一次证书未到期之前任务会跳过不会重复执行", triggerCronHelper: "点击上面的按钮,选择每天几点定时执行。\n建议设置为每天触发一次证书未到期之前任务会跳过不会重复执行",
notificationTitle: "失败通知", notificationTitle: "失败通知",
notificationWhen: "通知时机",
notificationHelper: "任务执行失败实时提醒", notificationHelper: "任务执行失败实时提醒",
groupIdTitle: "流水线分组", groupIdTitle: "流水线分组",
}, },
@@ -570,7 +572,7 @@ export default {
ipv6Priority: "IPV6优先", ipv6Priority: "IPV6优先",
dualStackNetworkHelper: "如果选择IPv6优先需要在docker-compose.yaml中启用ipv6", dualStackNetworkHelper: "如果选择IPv6优先需要在docker-compose.yaml中启用ipv6",
enableCommonCnameService: "启用公共CNAME服务", enableCommonCnameService: "启用公共CNAME服务",
commonCnameHelper: "是否可以使用公共CNAME服务如果禁用且没有设置<router-link to='/sys/cname/provider'>自定义CNAME服务</router-link>则无法使用CNAME代理方式申请证书", commonCnameHelper: "是否可以使用公共CNAME服务如果禁用且没有设置<a href='#/sys/cname/provider'>自定义CNAME服务</a>则无法使用CNAME代理方式申请证书",
enableCommonSelfServicePasswordRetrieval: "启用自助找回密码", enableCommonSelfServicePasswordRetrieval: "启用自助找回密码",
saveButton: "保存", saveButton: "保存",
stopSuccess: "停止成功", stopSuccess: "停止成功",
@@ -593,6 +595,7 @@ export default {
commFeature: "商业版功能", commFeature: "商业版功能",
smsProvider: "短信提供商", smsProvider: "短信提供商",
aliyunSms: "阿里云短信", aliyunSms: "阿里云短信",
tencentSms: "腾讯云短信",
yfySms: "易发云短信", yfySms: "易发云短信",
smsTest: "短信测试", smsTest: "短信测试",
testMobilePlaceholder: "输入测试手机号", testMobilePlaceholder: "输入测试手机号",
@@ -711,6 +714,10 @@ export default {
setting: { setting: {
showRunStrategy: "显示运行策略选择", showRunStrategy: "显示运行策略选择",
showRunStrategyHelper: "任务设置中是否允许选择运行策略", showRunStrategyHelper: "任务设置中是否允许选择运行策略",
captchaEnabled: "启用登录验证码",
captchaHelper: "登录时是否启用验证码",
captchaType: "验证码类型",
}, },
}, },
modal: { modal: {
@@ -731,4 +738,8 @@ export default {
challengeSetting: "校验配置", challengeSetting: "校验配置",
gotoCnameTip: "CNAME域名配置请前往CNAME记录页面添加", gotoCnameTip: "CNAME域名配置请前往CNAME记录页面添加",
}, },
addonSelector: {
select: "选择",
placeholder: "请选择",
},
}; };

View File

@@ -0,0 +1,12 @@
import { useSettingStore } from "/@/store/settings";
export default {
mounted(el: any, binding: any, vnode: any) {
const settingStore = useSettingStore();
const isComm = settingStore.isComm;
const { value } = binding;
if ((value === false && isComm) || (value === true && !isComm)) {
el.parentNode && el.parentNode.removeChild(el);
}
},
};

View File

@@ -0,0 +1,8 @@
import comm from "./comm-show.js";
const install = function (app: any) {
app.directive("comm", comm);
};
export default {
install,
};

View File

@@ -4,9 +4,12 @@ import FastCrud from "./fast-crud";
import permission from "./permission"; import permission from "./permission";
import { App } from "vue"; import { App } from "vue";
import "./validator/index.js"; import "./validator/index.js";
import directives from "./directive/index";
function install(app: App, options: any = {}) { function install(app: App, options: any = {}) {
app.use(FastCrud, options); app.use(FastCrud, options);
app.use(permission); app.use(permission);
app.use(directives);
} }
export default { export default {

View File

@@ -133,7 +133,7 @@ export const sysResources = [
title: "certd.sysResources.sysPluginConfig", title: "certd.sysResources.sysPluginConfig",
name: "SysPluginConfig", name: "SysPluginConfig",
path: "/sys/plugin/config", path: "/sys/plugin/config",
component: "/sys/plugin/config.vue", component: "/sys/plugin/config-common.vue",
meta: { meta: {
show: () => { show: () => {
const settingStore = useSettingStore(); const settingStore = useSettingStore();

View File

@@ -1,9 +1,11 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import * as api from "./api.plugin"; import * as api from "./api.plugin";
import { DynamicType, FormItemProps } from "@fast-crud/fast-crud"; import { DynamicType, FormItemProps, useMerge } from "@fast-crud/fast-crud";
import { i18n } from "/src/locales/i18n"; import { i18n } from "/src/locales/i18n";
import { cloneDeep } from "lodash-es";
interface PluginState { interface PluginState {
group?: PluginGroups; group?: PluginGroups;
originGroup?: PluginGroups;
} }
export type PluginGroup = { export type PluginGroup = {
@@ -32,14 +34,17 @@ export class PluginGroups {
groups!: { [key: string]: PluginGroup }; groups!: { [key: string]: PluginGroup };
map!: { [key: string]: PluginDefine }; map!: { [key: string]: PluginDefine };
t: any; t: any;
constructor(groups: { [key: string]: PluginGroup }) { mergeSetting?: boolean;
constructor(groups: { [key: string]: PluginGroup }, opts?: { mergeSetting?: boolean }) {
this.groups = groups; this.groups = groups;
this.t = i18n.global.t; this.t = i18n.global.t;
this.mergeSetting = opts?.mergeSetting ?? false;
this.initGroup(groups); this.initGroup(groups);
this.initMap(); this.initMap();
} }
private initGroup(groups: { [p: string]: PluginGroup }) { private initGroup(groups: { [p: string]: PluginGroup }) {
const { merge } = useMerge();
const all: PluginGroup = { const all: PluginGroup = {
key: "all", key: "all",
title: this.t("certd.all"), title: this.t("certd.all"),
@@ -48,6 +53,14 @@ export class PluginGroups {
icon: "material-symbols:border-all-rounded", icon: "material-symbols:border-all-rounded",
}; };
for (const key in groups) { 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 || {});
}
}
}
all.plugins.push(...groups[key].plugins); all.plugins.push(...groups[key].plugins);
} }
this.groups = { this.groups = {
@@ -132,11 +145,15 @@ export const usePluginStore = defineStore({
id: "app.plugin", id: "app.plugin",
state: (): PluginState => ({ state: (): PluginState => ({
group: null, group: null,
originGroup: null,
}), }),
actions: { actions: {
async reload() { async reload() {
const groups = await api.GetGroups({}); const groups = await api.GetGroups({});
this.group = new PluginGroups(groups); this.group = new PluginGroups(groups, { mergeSetting: true });
this.originGroup = new PluginGroups(cloneDeep(groups));
console.log("group", this.group);
console.log("originGroup", this.originGroup);
}, },
async init() { async init() {
if (!this.group) { if (!this.group) {
@@ -150,6 +167,7 @@ export const usePluginStore = defineStore({
}, },
async clear() { async clear() {
this.group = null; this.group = null;
this.originGroup = null;
}, },
async getList(): Promise<PluginDefine[]> { async getList(): Promise<PluginDefine[]> {
await this.init(); await this.init();
@@ -159,6 +177,10 @@ export const usePluginStore = defineStore({
await this.init(); await this.init();
return this.group.get(name); return this.group.get(name);
}, },
async getPluginDefineFromOrigin(name: string): Promise<PluginDefine> {
await this.init();
return this.originGroup.get(name);
},
async getPluginConfig(query: any) { async getPluginConfig(query: any) {
return await api.GetPluginConfig(query); return await api.GetPluginConfig(query);
}, },

View File

@@ -46,6 +46,10 @@ export type SysPublicSetting = {
aiChatEnabled?: boolean; aiChatEnabled?: boolean;
showRunStrategy?: boolean; showRunStrategy?: boolean;
captchaEnabled?: boolean;
captchaType?: number;
captchaAddonId?: number;
}; };
export type SuiteSetting = { export type SuiteSetting = {
enabled?: boolean; enabled?: boolean;

View File

@@ -12,6 +12,7 @@ import { utils } from "/@/utils";
import { cloneDeep, merge } from "lodash-es"; import { cloneDeep, merge } from "lodash-es";
import { useI18n } from "/src/locales"; import { useI18n } from "/src/locales";
export interface SettingState { export interface SettingState {
skipReset?: boolean; // 注销登录时不清空此store的状态
sysPublic?: SysPublicSetting; sysPublic?: SysPublicSetting;
installInfo?: { installInfo?: {
siteId: string; siteId: string;
@@ -64,6 +65,7 @@ const defaultSiteInfo: SiteInfo = {
export const useSettingStore = defineStore({ export const useSettingStore = defineStore({
id: "app.setting", id: "app.setting",
state: (): SettingState => ({ state: (): SettingState => ({
skipReset: true,
plusInfo: { plusInfo: {
isPlus: false, isPlus: false,
vipType: "free", vipType: "free",

View File

@@ -304,3 +304,11 @@ h6 {
padding: 10px; padding: 10px;
color: #6e6e6e; color: #6e6e6e;
} }
.ant-modal-body{
.fs-form-body{
max-height: 66vh;
overflow-y: auto;
}
}

View File

@@ -38,6 +38,9 @@ export function resetAllStores() {
} }
const allStores = (pinia as any)._s; const allStores = (pinia as any)._s;
for (const [_key, store] of allStores) { for (const [_key, store] of allStores) {
if (store.skipReset) {
continue;
}
store.$reset(); store.$reset();
} }
} }

View File

@@ -84,6 +84,7 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
component: { component: {
color: "auto", color: "auto",
}, },
order: -1,
}, },
form: { form: {
component: { component: {

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