Compare commits

...
52 Commits
Author SHA1 Message Date
xiaojunnuo 67f3daae62 v1.44.2 2026-09-03 22:04:19 +08:00
xiaojunnuo c72fa68e4c build: prepare to build 2026-09-03 21:57:56 +08:00
xiaojunnuo 1f5cda452c test(cert): 修复DNS持久化记录服务测试用例的查询方法适配问题
将测试中使用的findOneBy替换为标准的findOne方法,适配服务层实际的查询逻辑,修正测试用例的模拟实现
2026-09-03 21:57:47 +08:00
xiaojunnuo 5a19d2513d build: prepare to build 2026-09-03 21:49:35 +08:00
xiaojunnuo c0f8145615 test(plugin-service): 完善插件服务测试用例
修复并补充插件安装相关的单元测试逻辑,新增findOne仓库方法模拟,校验查询参数正确性,调整断言逻辑以匹配实际的查询次数
2026-09-03 21:49:15 +08:00
xiaojunnuo 2ad14b82fa chore(base-service): 修复findOneBy方法调用不兼容的问题
将弃用的findOneBy替换为标准的findOne查询方式,兼容新版本的TypeORM接口
2026-09-03 21:37:52 +08:00
xiaojunnuo c8bdcdc778 build: prepare to build 2026-09-03 21:20:43 +08:00
xiaojunnuo 15d2bd8484 fix: 修复插件市场插件安装无效的bug 2026-09-03 21:20:17 +08:00
xiaojunnuo 7d4311115c chore(plugin-service): 补全内置插件的installed字段赋值逻辑
为内置插件添加默认的installed状态设置,统一插件安装状态的处理逻辑,修复部分场景下插件安装状态不准确的问题
2026-09-03 01:39:22 +08:00
xiaojunnuo 3efa0582ef Merge branch 'v2-dev' into v2 2026-09-03 01:17:35 +08:00
xiaojunnuo 7624babc99 build: release 2026-09-03 01:17:10 +08:00
xiaojunnuo 6cf8f350f5 build: publish 2026-09-03 00:58:57 +08:00
xiaojunnuo 0e08555220 build: trigger build image 2026-09-03 00:58:47 +08:00
xiaojunnuo fe738fa7d4 v1.44.1 2026-09-03 00:50:48 +08:00
xiaojunnuo 2439f4ab02 build: prepare to build 2026-09-03 00:44:44 +08:00
xiaojunnuo 75b2b146ce test: 优化两个测试文件的代码结构与测试逻辑
1.  为证书信息服务测试提取复用的服务创建函数,减少重复代码
2.  修复插件服务测试用例,修正作者校验逻辑、同步逻辑、插件导入更新等多处测试断言
2026-09-03 00:43:53 +08:00
xiaojunnuo 1f3c096517 build: prepare to build 2026-09-03 00:22:26 +08:00
xiaojunnuo d264388c00 build: prepare to build 2026-09-03 00:17:04 +08:00
xiaojunnuo b8eaeeaa64 refactor(plugin-service): 调整在线插件作者请求的必填字段
将name和email从必填改为可选字段,优化接口参数灵活性
2026-09-03 00:16:59 +08:00
xiaojunnuo dba1a486cb chore(ui/certd-server): update metadata config and fix event name
1. 修正deploy_CertApply.yaml中DNS持久验证的说明文本
2. 将表单的selected-change事件名改为驼峰式selectedChange
3. 为acmeAccount的选择组件添加emitImmediate配置
2026-09-03 00:15:49 +08:00
xiaojunnuo 5408ae6ad8 build: prepare to build 2026-09-03 00:08:37 +08:00
Atirna f5430a8925 fix: preserve Route53 TXT record values (#791) 2026-09-02 23:59:45 +08:00
xiaojunnuo 9cc21204b1 docs(install/docker): update mirror table and add aliyun registry tags
1. 补全docker镜像表格的分隔线对齐格式
2. 在release镜像工作流中新增阿里云certd镜像仓库的推送标签
2026-09-02 22:42:25 +08:00
xiaojunnuo f911e06a69 fix: 修复大量问题
1.  更新多处文案,将"仅Let's Encrypt"改为"仅Let's Encrypt 测试环境"
2.  新增getSiteId方法到设置store
3.  优化项目store缓存key和变量声明顺序
4.  重构插件作者字段组件:添加作者注册状态管理、禁用输入框、支持修改邮箱
5.  暴露updatePluginAuthorEmail方法并完善插件类型注释
6.  调整首页统计卡片布局位置
7.  移除多余空行和注释掉Addon插件类型选项
2026-09-01 23:21:06 +08:00
xiaojunnuo 4a6eab0ce6 refactor(plugin): 调整插件相关代码结构与逻辑
1. 为createRemoteSelectInputDefine新增emitImmediate参数
2. 新增BaseService.updateById方法简化更新操作
3. 移除插件服务中多余的YAML序列化工具方法,改为前端处理
4. 优化插件更新、同步逻辑,替换部分update调用为updateById
5. 简化插件作者校验逻辑
2026-09-01 20:53:50 +08:00
xiaojunnuo 1d90fadd0c Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-09-01 00:23:56 +08:00
xiaojunnuo 1b1a5ec94d refactor(dashboard): 重构统计逻辑,优化前端展示与代码结构
1.  重构用户统计数据模型与累加逻辑,使用lodash简化字段操作
2.  调整仪表盘布局与统计卡片样式,适配暗色模式
3.  优化插件系统默认值加载时机,修复配置同步问题
4.  简化多语言文案,统一统计数据展示格式
5.  清理冗余代码与注释,修复代码规范问题
2026-09-01 00:23:53 +08:00
xiaojunnuo d67379f297 fix: 修复编辑证书申请任务导致清空ACME账号的bug
这一版本新增了用户证书生成统计、流水线运行统计功能,优化了插件配置获取和更新流程:
1.  新增用户统计设置模型,统计单域名/多域名/泛域名证书生成数和流水线运行次数
2.  仪表盘新增累计证书数、分类统计和节省金额估算卡片
3.  重构插件配置相关接口,统一插件获取、配置保存逻辑
4.  修复远程选择组件初始值触发逻辑,新增emitImmediate配置项
5.  优化ACME账号切换逻辑,修复对象展开语法语法问题
6.  补充多语言国际化文案,调整部分前端样式细节
2026-08-31 22:54:28 +08:00
xiaojunnuo 249b98df66 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-08-31 13:23:47 +08:00
xiaojunnuo aebbb5cc7d docs: 更新DNS持久验证的说明,移除测试环境限制
将多处提及的"仅Let's Encrypt测试环境可以申请"修改为正式环境支持,同步更新所有相关提示文案
2026-08-31 13:23:40 +08:00
xiaojunnuo 4f020aa8a5 chore: 移除compile脚本中多余的rimraf前置步骤
统一移除多个package.json里compile命令前冗余的rimraf dist && 前缀,简化编译脚本命令
2026-08-30 10:56:42 +08:00
xiaojunnuo 02426c507a docs: update database install doc and docker compose config
1. 修正文档中PG的全称表述并补充执行SQL的前置条件
2. 新增PostgreSQL SSL连接配置注释到docker-compose示例
3. 更新自动升级注释说明支持stable版本
2026-08-30 10:51:06 +08:00
xiaojunnuo ffb7ae79eb Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-08-30 01:52:31 +08:00
xiaojunnuo f58f2f3cd6 fix: 修复旧流水线保存时清空证书仓库证书内容的bug 2026-08-30 01:52:25 +08:00
xiaojunnuo 794abc3d7a Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-08-30 00:43:53 +08:00
xiaojunnuo da406014ad chore(plugin): add online plugin author email update function
1. 新增修改在线插件作者邮箱的接口与前后端实现
2. 调整插件作者相关类型定义,精简冗余字段并添加email字段
3. 新增邮箱格式校验逻辑
4. 在插件发布页面添加修改作者邮箱的交互功能
2026-08-30 00:43:47 +08:00
xiaojunnuo 9842a24a50 perf(dashboard): 新增仪表盘站点监控和域名管理的统计卡片
1. 新增仪表盘站点监控和域名管理的统计卡片,包含总数、异常/过期状态统计
2. 补充多语言国际化文案(zh-CN和en-US)
3. 后端接口新增站点和域名的统计数据查询逻辑
4. 优化统计卡片样式,根据状态显示不同色调
5. 修复代码格式问题和冗余代码
2026-08-30 00:23:21 +08:00
xiaojunnuo fb21ec04a6 chore(scripts): 为release接口url添加v前缀
修正获取指定版本发布信息的接口地址,补上版本号前的v前缀,确保接口调用正确
2026-08-29 15:24:12 +08:00
xiaojunnuo 15b74207dd ci(github actions): 替换npm脚本为自定义node脚本
将原来通过npm调用的设置版本稳定状态的步骤,替换为直接调用自定义的node脚本,简化CI流程并移除额外的npm依赖安装步骤
2026-08-29 15:20:52 +08:00
xiaojunnuo 6055e9b6ba ci(github actions): 优化稳定版发布的npm安装步骤
仅安装指定版本的axios并跳过包锁和保存,减少不必要的依赖安装
2026-08-29 15:12:00 +08:00
xiaojunnuo df3e0814c4 chore: 移除package.json里的root本地链接依赖并调整CI安装步骤
1.  从package.json中删除了无用的root本地链接依赖
2.  简化CI步骤,移除全局安装axios的命令,直接执行npm install
2026-08-29 15:05:25 +08:00
xiaojunnuo 81f660dc49 ci(github actions): 为稳定发布工作流全局安装axios
修复发布流程中axios依赖缺失导致的执行失败问题
2026-08-29 15:03:13 +08:00
xiaojunnuo f56e2a07c6 ci(github-actions): 为稳定发布工作流添加npm安装步骤
在stable-release.yml的发布任务中新增npm install命令,确保执行发布脚本前依赖已正确安装
2026-08-29 14:59:44 +08:00
xiaojunnuo a2fce6cf0b ci(github actions): 更新Docker发布配置
1. 更换Docker Hub登录的密钥名称
2. 为buildx构建添加--provenance和--sbom参数关闭物料清单生成
2026-08-29 12:08:51 +08:00
xiaojunnuo ccb5356bfa ci(ci): 移除稳定版镜像的版本标签构建
更新文档以适配不再发布带版本后缀的稳定版镜像
2026-08-29 12:01:40 +08:00
xiaojunnuo f44084dfd2 chore(base-service): 修复更新方法未校验id和新增重复id问题
1. 在新增逻辑中删除param的id字段避免重复插入
2. 格式化更新方法的id非空校验代码块
2026-08-29 04:22:13 +08:00
xiaojunnuo 948d76f98f build: release 2026-08-29 04:15:09 +08:00
xiaojunnuo 6606f32678 fix(pipeline-controller): 处理非编辑场景下保留id的问题
在非编辑流程中删除请求参数里的id字段,避免意外更新已有数据
2026-08-29 03:52:54 +08:00
xiaojunnuo 6af12d49fc fix(pipeline): 修正流水线保存时的ID判断逻辑并增加参数校验
调整了流水线新增的判断条件,将原来的bean.id <=0改为bean.id ==0或null,同时增加了当bean.id小于0时抛出错误的参数校验逻辑,避免非法ID传入
2026-08-29 03:47:55 +08:00
xiaojunnuo 9decb49d04 refactor: 重构插件同步逻辑,优化插件数据处理流程
1.  调整插件同步时的字段处理逻辑,减少冗余数据存储
2.  提前查询已有插件数据,避免重复创建和更新
3.  拆分插件同步代码结构,提升可读性和可维护性
4.  注释掉下载次数同步逻辑,暂时关闭该功能
5.  简化插件编辑接口的字段处理逻辑
6.  移除插件列表页的复制按钮,新增插件导出功能
7.  更新文档和镜像配置,修正Docker Hub镜像地址
8.  调整文档章节顺序,新增版本号徽章展示
2026-08-29 02:48:56 +08:00
xiaojunnuo 69466c1c41 build: publish 2026-08-29 00:46:02 +08:00
xiaojunnuo adf5163a79 build: trigger build image 2026-08-29 00:45:51 +08:00
90 changed files with 1466 additions and 631 deletions
+21 -15
View File
@@ -92,6 +92,8 @@ jobs:
tags: |
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}
registry.cn-shenzhen.aliyuncs.com/certd/certd:latest
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}
greper/certd:latest
greper/certd:${{steps.get_certd_version.outputs.result}}
ghcr.io/${{ github.repository }}:latest
@@ -111,8 +113,8 @@ jobs:
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-slim
registry.cn-shenzhen.aliyuncs.com/certd/certd:slim
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-slim
certd/certd:slim
certd/certd:${{steps.get_certd_version.outputs.result}}-slim
greper/certd:slim
greper/certd:${{steps.get_certd_version.outputs.result}}-slim
ghcr.io/${{ github.repository }}:slim
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-slim
@@ -129,22 +131,26 @@ jobs:
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7
registry.cn-shenzhen.aliyuncs.com/certd/certd:armv7
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-armv7
certd/certd:armv7
certd/certd:${{steps.get_certd_version.outputs.result}}-armv7
greper/certd:armv7
greper/certd:${{steps.get_certd_version.outputs.result}}-armv7
ghcr.io/${{ github.repository }}:armv7
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-armv7
# - name: Build agent
# uses: docker/build-push-action@v6
# with:
# platforms: linux/amd64,linux/arm64
# push: true
# context: ./packages/ui/agent/
# tags: |
# registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:latest
# registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:${{steps.get_certd_version.outputs.result}}
# greper/certd-agent:latest
# greper/certd-agent:${{steps.get_certd_version.outputs.result}}
- name: Login to Certd Docker Hub account
uses: docker/login-action@v3
with:
username: ${{ secrets.dockerhub_username2 }}
password: ${{ secrets.dockerhub_password2 }}
- name: Publish to certd Docker Hub repository
run: |
docker buildx imagetools create --tag certd/certd:latest greper/certd:latest
docker buildx imagetools create --tag certd/certd:${{ steps.get_certd_version.outputs.result }} greper/certd:${{ steps.get_certd_version.outputs.result }}
docker buildx imagetools create --tag certd/certd:slim greper/certd:slim
docker buildx imagetools create --tag certd/certd:${{ steps.get_certd_version.outputs.result }}-slim greper/certd:${{ steps.get_certd_version.outputs.result }}-slim
docker buildx imagetools create --tag certd/certd:armv7 greper/certd:armv7
docker buildx imagetools create --tag certd/certd:${{ steps.get_certd_version.outputs.result }}-armv7 greper/certd:${{ steps.get_certd_version.outputs.result }}-armv7
- name: deploy-certd-doc
uses: tyrrrz/action-http-request@prime
with:
+7 -11
View File
@@ -40,8 +40,8 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.dockerhub_username }}
password: ${{ secrets.dockerhub_password }}
username: ${{ secrets.dockerhub_username2 }}
password: ${{ secrets.dockerhub_password2 }}
# stable 镜像:叠加 ENV 层
- name: Build and push stable images
@@ -50,16 +50,14 @@ jobs:
ENV certd_release_mode=stable" > Dockerfile.stable
docker buildx build \
--platform linux/amd64,linux/arm64 \
--provenance=false \
--sbom=false \
--push \
-f Dockerfile.stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{ inputs.version }}-stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:${{ inputs.version }}-stable \
-t certd/certd:stable \
-t certd/certd:${{ inputs.version }}-stable \
-t ghcr.io/${{ github.repository }}:stable \
-t ghcr.io/${{ github.repository }}:${{ inputs.version }}-stable \
.
# slim-stable 镜像
@@ -69,20 +67,18 @@ jobs:
ENV certd_release_mode=stable" > Dockerfile.slim-stable
docker buildx build \
--platform linux/amd64,linux/arm64 \
--provenance=false \
--sbom=false \
--push \
-f Dockerfile.slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{ inputs.version }}-slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:${{ inputs.version }}-slim-stable \
-t certd/certd:slim-stable \
-t certd/certd:${{ inputs.version }}-slim-stable \
-t ghcr.io/${{ github.repository }}:slim-stable \
-t ghcr.io/${{ github.repository }}:${{ inputs.version }}-slim-stable \
.
- name: Set AtomGit release as stable
run: |
export ATOMGIT_TOKEN=${{ secrets.ATOMGIT_TOKEN }}
export VERSION=${{ inputs.version }}
npm run set-release-stable
node ./scripts/set-release-stable-fetch.js
+2 -1
View File
@@ -75,11 +75,12 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 只有真正机械化的大批量替换、格式化或生成任务才考虑脚本/工具。若必须使用临时脚本,应放在临时目录并在同一个受控步骤内完成创建、执行、删除;不要把临时脚本落在仓库里跨多步工具调用执行。
- 不要主动运行 `pnpm install`;缺依赖、TTY、网络导致安装或测试失败时,停止尝试并说明环境问题。
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
- 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
- 代码可读性优先于短写法。尽量少用三元表达式,优先使用完整的 `if/else`;复杂条件、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
- 方法调用链不要直接塞进另一个方法参数;先用有意义的局部变量承接返回值,再传入下一步。
- 不要在单一表达式内嵌套分支、对象构造与方法调用。优先使用清晰的 `if/else` 分支;仅在确实能降低复杂度时才提取有意义的中间变量,避免为拆分而增加阅读跳转。
- 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。
- 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。
- 如无明确复用价值,不要随意新增 `normalizeXxx` 等辅助方法;优先在现有业务方法中直接处理简单兼容或字段转换逻辑,避免增加不必要的抽象层。
## 测试与验证
+21
View File
@@ -3,6 +3,27 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
### Bug Fixes
* 修复插件市场插件安装无效的bug ([15d2bd8](https://github.com/certd/certd/commit/15d2bd8484a9ddd142bf4ef03d839c74405f9e6d))
* preserve Route53 TXT record values ([#791](https://github.com/certd/certd/issues/791)) ([f5430a8](https://github.com/certd/certd/commit/f5430a8925cf6cf4e16c75f2358fbd78437e6705))
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
### Bug Fixes
* 修复编辑证书申请任务导致清空ACME账号的bug ([d67379f](https://github.com/certd/certd/commit/d67379f29725bbd6dcdefe97d101023c7fd11f03))
* 修复大量问题 ([f911e06](https://github.com/certd/certd/commit/f911e06a698d568ae228880ec89b7e25fdd1305b))
* 修复旧流水线保存时清空证书仓库证书内容的bug ([f58f2f3](https://github.com/certd/certd/commit/f58f2f3cd603c37e55ac5868fdd2ca99aa937e44))
* **pipeline-controller:** 处理非编辑场景下保留id的问题 ([6606f32](https://github.com/certd/certd/commit/6606f3267868e3aadaca0092ef4c83198312553c))
* **pipeline:** 修正流水线保存时的ID判断逻辑并增加参数校验 ([6af12d4](https://github.com/certd/certd/commit/6af12d49fce1144943dc37ba574b3c9356998dcd))
### Performance Improvements
* **dashboard:** 新增仪表盘站点监控和域名管理的统计卡片 ([9842a24](https://github.com/certd/certd/commit/9842a24a50a841f710ee7c8050a38e3334c58394))
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Bug Fixes
+13 -12
View File
@@ -109,7 +109,8 @@ https://certd.handfree.work/
```
registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型
------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型 (中括号表示可选)
```
##### 2. 版本标签:
@@ -124,25 +125,25 @@ registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
> 根据基础镜像不同,分为如下三个分支版本,没有特殊需求选择默认的即可(他们功能是一样的)
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 |
| --- | --- | --- | --- | --- |
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小 | `certd:stable` |
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` |
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` |
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 | 指定版本 |
| --- | --- | --- | --- | --- | --- |
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小 | `certd:stable` | `certd:1.43.0` |
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` | `certd:1.43.0-slim` |
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` | `certd:1.43.0-armv7` |
##### 2. 镜像地址:
##### 3. 镜像地址:
| 镜像仓库 | 最新预览版 | slim | armv7 |
| --- | --- | --- | --- |
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` |
| Docker Hub | `greper/certd:latest` | `certd:slim` |
| Docker Hub | `certd/certd:latest` | `certd:slim` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` |
> 注意:
> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
> 2. 稳定版在后面加 `-stable` 即可。
> 3. 如需指定具体的版本号,在冒号后面加 `version-`即可,例如 `certd:1.42.1-stable`。
> 2. 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
最新版本号:
![GitHub release (latest by date)](https://img.shields.io/github/v/release/certd/certd?label=Latest%20Release&sort=semver)
##### 3. 镜像构建说明:
+9 -3
View File
@@ -2,9 +2,9 @@ version: '3.3' # 兼容旧版docker-compose
services:
certd:
# 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如:certd:1.29.0
image: registry.cn-shenzhen.aliyuncs.com/certd/certd:latest
image: registry.cn-shenzhen.aliyuncs.com/certd/certd:latest # 稳定版为 certd/certd:stable
# image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源
# image: greper/certd:latest
# image: certd/certd:latest
# --------- 生产建议使用稳定版, latest改成stable即可
# image: registry.cn-shenzhen.aliyuncs.com/certd/certd:stable
@@ -77,7 +77,13 @@ services:
# - certd_typeorm_dataSource_default_password=yourpasswd # 密码
# - certd_typeorm_dataSource_default_database=certd # 数据库名
# ↓↓↓↓ --------------------------------------------------------- 自动升级,上面certd的版本号要保持为latest
# #↓↓↓↓ ---------------------------- 如果数据库服务端开启了加密传输 require_secure_transport-ON
# - certd_typeorm_dataSource_default_ssl_rejectUnauthorized=false # 忽略证书校验,或者配置下面的三项路径
# - certd_typeorm_dataSource_default_ssl_ca # 证书CA路径【如果上面false,无需配置】
# - certd_typeorm_dataSource_default_ssl_key # 私钥路径【如果上面false,无需配置】
# - certd_typeorm_dataSource_default_ssl_cert # 证书路径【如果上面false,无需配置】
# ↓↓↓↓ --------------------------------------------------------- 自动升级,上面certd的版本号要保持为latest / stable
# certd-updater: # 添加 Watchtower 服务
# image: containrrr/watchtower:latest
# container_name: certd-updater
+54
View File
@@ -3,6 +3,60 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
### Bug Fixes
* 修复编辑证书申请任务导致清空ACME账号的bug ([d67379f](https://github.com/certd/certd/commit/d67379f29725bbd6dcdefe97d101023c7fd11f03))
* 修复大量问题 ([f911e06](https://github.com/certd/certd/commit/f911e06a698d568ae228880ec89b7e25fdd1305b))
* 修复旧流水线保存时清空证书仓库证书内容的bug ([f58f2f3](https://github.com/certd/certd/commit/f58f2f3cd603c37e55ac5868fdd2ca99aa937e44))
* **pipeline-controller:** 处理非编辑场景下保留id的问题 ([6606f32](https://github.com/certd/certd/commit/6606f3267868e3aadaca0092ef4c83198312553c))
* **pipeline:** 修正流水线保存时的ID判断逻辑并增加参数校验 ([6af12d4](https://github.com/certd/certd/commit/6af12d49fce1144943dc37ba574b3c9356998dcd))
### Performance Improvements
* **dashboard:** 新增仪表盘站点监控和域名管理的统计卡片 ([9842a24](https://github.com/certd/certd/commit/9842a24a50a841f710ee7c8050a38e3334c58394))
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Bug Fixes
* 修复证书申请时EAB判断逻辑,已选择Acme账号的情况下不需要EAB ([d87821e](https://github.com/certd/certd/commit/d87821ee02b17136fc925df4547223332bf22823))
* **pipeline:** 校验定时触发器cron表达式,拦截2月31号等无效日期 ([#785](https://github.com/certd/certd/issues/785)) [@yukaidi1220](https://github.com/yukaidi1220) ([f8e492d](https://github.com/certd/certd/commit/f8e492d6cb49257e56bbd06b28fd6eb32c923c28))
* **plugin-ai-dev:** 优化AI插件测试流程并修复日志获取问题 ([faf9828](https://github.com/certd/certd/commit/faf9828168f6ce82adc530a65eaf313d6c9904b5))
* **plugin-item-card:** 移除分数格式化时去掉.0的逻辑 ([a91d7c0](https://github.com/certd/certd/commit/a91d7c000a2aa6456a249433ff90a47787ec90f0))
* **ssh:** 修复 keyboard-interactive 认证可能抛类型错误的问题 ([5e867c8](https://github.com/certd/certd/commit/5e867c86c30eb43f1e5aeee8c22d47596e173a8c))
* **start.sh:** 优化前端zip包下载校验逻辑 ([15ee6ea](https://github.com/certd/certd/commit/15ee6ead49db3620d2ade641aadbfcb13ac3e6dc))
* **tencent access:** 修复腾讯云国际站接口endpoint拼接问题 ([f614872](https://github.com/certd/certd/commit/f61487284f79d3f5a72c9f7eee04999a93728326))
### Features
* 实现自定义ACME证书颁发机构支持 ([97c417b](https://github.com/certd/certd/commit/97c417b1ad64b9a14bee8bfe98a2e789b9278820))
* 新增证书吊销后置任务与证书状态管理 ([1cc9641](https://github.com/certd/certd/commit/1cc9641f5669710d1e5809995ad395c843855a12))
* 新增AI插件开发全流程能力 ([6f9dffe](https://github.com/certd/certd/commit/6f9dffe6e59cc125bd394d7db6086d66a9c71e49))
* 支持审计日志,操作日志 ([f2855d6](https://github.com/certd/certd/commit/f2855d6dac74c9db71d92f99601e35c360b10666))
* 支持在线插件下载安装审核等 ([255960d](https://github.com/certd/certd/commit/255960d434eebb3d567a08f737f1642ab417b5d0))
### Performance Improvements
* 统一镜像地址格式 certd/certd ([49007d3](https://github.com/certd/certd/commit/49007d391565099d94dec375eb88e946856506b0))
* 新增AI插件开发受限令牌与隔离API ([3a9069f](https://github.com/certd/certd/commit/3a9069f6e6242940229fd63c686f7e445408dbbb))
* 优化源码启动脚本,增加快速启动 ([0cfc3b7](https://github.com/certd/certd/commit/0cfc3b7ae90fa031d75f0b395556a04d4efee76c))
* 统一将路由前缀从/certd改为/cert ([4db0f83](https://github.com/certd/certd/commit/4db0f83304dc6fd9199673c48b1805421f024601))
* 新增官方客户端Certd Client的多语言配置与页面入口 ([fbfe125](https://github.com/certd/certd/commit/fbfe1256a5c8be6dfb2fbae7e6130eb5f6c6d946))
* 新增证书吊销功能,添加证书状态字段 ([5074572](https://github.com/certd/certd/commit/5074572694383c8e1bf2880e26827987683dc62c))
* 新增自定义ACME管理功能,完善证书颁发机构适配 ([2ab23f5](https://github.com/certd/certd/commit/2ab23f50b216e27495fc823d2269ca0ba6366d15))
* 移除全局反向代理配置,迁移到自定义ACME配置 ([c57110e](https://github.com/certd/certd/commit/c57110e42aada31dd0028b56b5baec1b19722525))
* 在线插件支持通知类型 ([c0f194f](https://github.com/certd/certd/commit/c0f194f4438bd7ca3f52f63ca088543abde7428c))
* 增加稳定版,预览版区分,预览版是最新版,稳定版在预览版验证稳定后,再将预览版转为稳定版 ([1574090](https://github.com/certd/certd/commit/15740904e5e97ee767c39e4401dbc1d645795c38))
* 支持AI开发在线插件 ([eae7edb](https://github.com/certd/certd/commit/eae7edbda0fd7357163f33fb4084fcba21396f5f))
* **domain:** 域名管理增加remark字段 ([e4041ea](https://github.com/certd/certd/commit/e4041eabece1bab2c1c4b955129cf7a7fe7302d0))
* **login,settings:** 仅第三方登录开启后,在重启10分钟后将无法使用密码登录 ([1d12ad7](https://github.com/certd/certd/commit/1d12ad73c6ca827d2eb36bda867f9d9fe62dab48))
* **plugin:** 插件安装支持检测依赖 ([e79660c](https://github.com/certd/certd/commit/e79660c5d17ed4049508a00a359950a54470332f))
* **plugin:** 新增插件评分展示、排序与个人插件筛选功能 ([7ab5029](https://github.com/certd/certd/commit/7ab502991e0c001777b268b9f159311ef5e000a0))
* **server:** response优化为gzip压缩 ([f38522c](https://github.com/certd/certd/commit/f38522ca0b2185f7f35538f53cb51dcf86d0bfc8))
* **tencent-clb:** 重构腾讯云CLB部署插件的地域选择组件,改为远程下拉选择器 ([30d4fc8](https://github.com/certd/certd/commit/30d4fc84a9e940d1d07e1f25856eb6fa13da9ba1))
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
### Bug Fixes
+2 -2
View File
@@ -125,9 +125,9 @@ docker-compose up -d
## 三、问题
### 1. 迁移到PG之后,所有表的Sequence会从1开始
### 1. 迁移到PostgreSQL之后,所有表的Sequence会从1开始
执行如下sql 语句,批量将所有表的id自增设置到当前数据的最大值
执行如下sql 语句,批量将所有表的id自增设置到当前数据的最大值【仅迁移到postgresql需要执行】
```sql
DO $$
DECLARE
+33 -10
View File
@@ -6,22 +6,45 @@
Certd 提供多种 Docker 镜像版本,您可以根据需要选择:
**最新版本:**
#### 1. 镜像地址格式:
```
registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型
```
#### 2. 版本标签:
**最新版本标签:**
| 版本 | 标签 | 说明 |
| --- | --- | --- |
| 预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 |
| 稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 |
| 最新预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 |
| 最新稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 |
**系统版本分支:**
**系统分支版本:**
| 分支版本标签 | 基础系统 | 说明 | 指定版本 | 稳定版 |
| --- | --- | --- | --- | --- |
| `certd:latest` 【默认】 | Alpine Linux | 默认版本,镜像体积小 | `certd:[version]` | `certd:[version-]stable` |
| `certd:slim` | Debian slim | glibc版本,dns解析兼容性更好(可能需要配置security_opt -seccomp=unconfined)| `certd:[version-]slim` | `certd:[version-]slim-stable` |
| `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:[version]-armv7` | `certd:[version-]armv7-ststable` |
> 根据基础镜像不同,分为如下三个分支版本,没有特殊需求选择默认的即可(他们功能是一样的)
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 | 指定版本 |
| --- | --- | --- | --- | --- | --- |
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小,支持x86、ARM架构 | `certd:stable` | `certd:1.43.0` |
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` | `certd:1.43.0-slim` |
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` | `certd:1.43.0-armv7` |
#### 3. 镜像地址:
| 镜像仓库 | 最新预览版 | slim版 | armv7版 |
| --- | --- | --- | --- |
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` |
| Docker Hub | `certd/certd:latest` | `certd:slim` | `certd:armv7` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` |
> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
> 2. 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
最新版本号:
![GitHub release (latest by date)](https://img.shields.io/github/v/release/certd/certd?label=Latest%20Release&sort=semver)
> 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
### 一键脚本安装(推荐)
+1 -1
View File
@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.44.0"
"version": "1.44.2"
}
-1
View File
@@ -53,7 +53,6 @@
"dependencies": {
"copyfiles": "^2.4.1",
"lodash-es": "^4.17.21",
"root": "link:",
"typescript": "^5.4.2"
},
"workspaces": [
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/publishlab/node-acme-client/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/acme-client
## [1.44.1](https://github.com/publishlab/node-acme-client/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/acme-client
# [1.44.0](https://github.com/publishlab/node-acme-client/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+4 -4
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.44.0",
"version": "1.44.2",
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.44.0",
"@certd/basic": "^1.44.2",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -58,7 +58,7 @@
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "rimraf dist && tsc --skipLibCheck --watch",
"compile": " tsc --skipLibCheck --watch",
"format": "prettier --write src"
},
"repository": {
@@ -76,5 +76,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/basic
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/basic
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+1 -1
View File
@@ -1 +1 @@
00:39
21:57
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.44.0",
"version": "1.44.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -15,7 +15,7 @@
"test": "mocha --loader=ts-node/esm",
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "rimraf dist && tsc --skipLibCheck --watch",
"compile": " tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
@@ -54,5 +54,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/pipeline
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/pipeline
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.44.0",
"version": "1.44.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,13 +16,13 @@
"test": "mocha --loader=ts-node/esm",
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "rimraf dist && tsc --skipLibCheck --watch",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.44.0",
"@certd/plus-core": "^1.44.0",
"@certd/basic": "^1.44.2",
"@certd/plus-core": "^1.44.2",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.2.2"
@@ -51,5 +51,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/lib-huawei
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/lib-huawei
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.44.0",
"version": "1.44.2",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -13,7 +13,7 @@
"preview": "vite preview",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish",
"compile": "rimraf dist && npm run build",
"compile": " npm run build",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
@@ -31,5 +31,5 @@
"prettier": "3.3.3",
"tslib": "^2.8.1"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/lib-iframe
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/lib-iframe
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.44.0",
"version": "1.44.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,7 +16,7 @@
"preview": "vite preview",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish",
"compile": "rimraf dist && npm run build",
"compile": " npm run build",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
@@ -37,5 +37,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/jdcloud
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/jdcloud
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
**Note:** Version bump only for package @certd/jdcloud
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.44.0",
"version": "1.44.2",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -63,5 +63,5 @@
"fetch"
]
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/lib-k8s
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/lib-k8s
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.44.0",
"version": "1.44.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,12 +16,12 @@
"preview": "vite preview",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish",
"compile": "rimraf dist && tsc --skipLibCheck --watch",
"compile": " tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.44.0",
"@certd/basic": "^1.44.2",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/lib-server
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/lib-server
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.44.0",
"version": "1.44.2",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -17,7 +17,7 @@
"cov": "midway-bin cov --ts",
"prepublish": "npm run build",
"pub": "npm publish",
"compile": "rimraf dist && tsc --skipLibCheck --watch",
"compile": " tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
@@ -29,11 +29,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.44.0",
"@certd/basic": "^1.44.0",
"@certd/pipeline": "^1.44.0",
"@certd/plugin-lib": "^1.44.0",
"@certd/plus-core": "^1.44.0",
"@certd/acme-client": "^1.44.2",
"@certd/basic": "^1.44.2",
"@certd/pipeline": "^1.44.2",
"@certd/plugin-lib": "^1.44.2",
"@certd/plus-core": "^1.44.2",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -69,5 +69,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
}
@@ -75,7 +75,10 @@ export abstract class BaseService<T> {
if (!id) {
throw new ValidateException("id不能为空");
}
const info = await this.getRepository().findOneBy({ id } as any);
const info = await this.getRepository().findOne({
//@ts-ignore
where: { id },
});
if (info && infoIgnoreProperty) {
for (const property of infoIgnoreProperty) {
delete info[property];
@@ -114,6 +117,13 @@ export abstract class BaseService<T> {
);
}
async updateById(data: any) {
if (!data.id) {
throw new ValidateException("id 不能为空");
}
await this.addOrUpdate(data);
}
/**
* 删除
* @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3
@@ -163,6 +173,7 @@ export abstract class BaseService<T> {
const now = new Date();
param.createTime = now;
param.updateTime = now;
delete param.id;
await this.addOrUpdate(param);
await this.modifyAfter(param);
return {
@@ -175,7 +186,9 @@ export abstract class BaseService<T> {
* @param param 数据
*/
async update(param: any) {
if (!param.id) throw new ValidateException("id 不能为空");
if (!param.id) {
throw new ValidateException("id 不能为空");
}
param.updateTime = new Date();
await this.addOrUpdate(param);
await this.modifyAfter(param);
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/midway-flyway-js
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Features
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.44.0",
"version": "1.44.2",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -17,7 +17,7 @@
"cov": "midway-bin cov --ts",
"prepublish": "npm run build",
"pub": "npm publish",
"compile": "rimraf dist && npm run build",
"compile": " npm run build",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
@@ -52,5 +52,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
}
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/plugin-cert
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/plugin-cert
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
**Note:** Version bump only for package @certd/plugin-cert
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.44.0",
"version": "1.44.2",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -20,7 +20,7 @@
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/plugin-lib": "^1.44.0"
"@certd/plugin-lib": "^1.44.2"
},
"devDependencies": {
"@types/chai": "^4.3.12",
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/plugin-lib
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
**Note:** Version bump only for package @certd/plugin-lib
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Bug Fixes
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.44.0",
"version": "1.44.2",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -12,14 +12,14 @@
"dev-build": "npm run build",
"test:unit": "cross-env NODE_ENV=unittest mocha",
"pub": "npm publish",
"compile": "rimraf dist && tsc --skipLibCheck --watch",
"compile": " tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/acme-client": "^1.44.0",
"@certd/basic": "^1.44.0",
"@certd/pipeline": "^1.44.0",
"@certd/acme-client": "^1.44.2",
"@certd/basic": "^1.44.2",
"@certd/pipeline": "^1.44.2",
"dayjs": "^1.11.7",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
@@ -45,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "5558376baee9e3568b00a8355a23898427efadf8"
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
}
@@ -40,6 +40,7 @@ export function createRemoteSelectInputDefine(opts?: {
rules?: any;
mergeScript?: string;
search?: boolean;
emitImmediate?: boolean;
pager?: boolean;
component?: any;
value?: any;
+18
View File
@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
### Bug Fixes
* 修复插件市场插件安装无效的bug ([15d2bd8](https://github.com/certd/certd/commit/15d2bd8484a9ddd142bf4ef03d839c74405f9e6d))
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
### Bug Fixes
* 修复编辑证书申请任务导致清空ACME账号的bug ([d67379f](https://github.com/certd/certd/commit/d67379f29725bbd6dcdefe97d101023c7fd11f03))
* 修复大量问题 ([f911e06](https://github.com/certd/certd/commit/f911e06a698d568ae228880ec89b7e25fdd1305b))
* **pipeline:** 修正流水线保存时的ID判断逻辑并增加参数校验 ([6af12d4](https://github.com/certd/certd/commit/6af12d49fce1144943dc37ba574b3c9356998dcd))
### Performance Improvements
* **dashboard:** 新增仪表盘站点监控和域名管理的统计卡片 ([9842a24](https://github.com/certd/certd/commit/9842a24a50a841f710ee7c8050a38e3334c58394))
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.44.0",
"version": "1.44.2",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -104,8 +104,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.44.0",
"@certd/pipeline": "^1.44.0",
"@certd/lib-iframe": "^1.44.2",
"@certd/pipeline": "^1.44.2",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -399,7 +399,7 @@ watch(
width: 80px;
}
.input {
width: 150px;
width: 180px;
}
}
}
@@ -71,6 +71,7 @@ const props = defineProps<
pager?: boolean;
single?: boolean;
pageSize?: number;
emitImmediate?: boolean;
uploadCert?: UploadCertProps;
} & ComponentPropsType
>();
@@ -210,8 +211,9 @@ const getOptions = async () => {
if (isFirst) {
isFirst = false;
if (props.value) {
if (props.value && props.emitImmediate) {
emitSelectedChange(props.value);
emit("update:value", props.value);
}
return;
}
@@ -14,6 +14,15 @@ export default {
certCount: "Number of Certificates",
noCert: "You have no certificates yet",
manageCert: "View Certificates",
siteMonitorCount: "Number of Monitored Sites",
siteAbnormalCount: "Abnormal",
siteNormalCount: "Normal",
manageSiteMonitor: "Manage Monitored Sites",
domainCount: "Number of Domains",
domainExpiringCount: "Expiring",
domainExpiredCount: "Expired",
domainNotExpiredCount: "Not Expired",
manageDomain: "Manage Domains",
certExpiringCount: "Soon-to-Expire",
certExpiredCount: "Expired",
certNoExpireCount: "Not Expired",
@@ -23,6 +32,16 @@ export default {
recentRun: "Recent Run Statistics",
runCount: "Run Count",
expiringCerts: "Soon-to-Expire Certificates",
totalGeneratedCertCount: "Total Certificates Generated",
singleDomainCertShortCount: "Single-domain",
multiDomainCertShortCount: "Multi-domain",
wildcardCertShortCount: "Wildcard",
singleDomainCertCount: "Single-domain Certificates Generated",
multiDomainCertCount: "Multi-domain Certificates Generated",
wildcardCertCount: "Wildcard Certificates Generated",
savedAmount: "Estimated savings",
savedAmountRule: "Estimated at one quarter of Tencent Cloud's annual certificate prices: single-domain ¥532, multi-domain ¥912, and wildcard ¥2147. Actual savings may vary.",
totalPipelineRuns: "Total Pipeline Runs",
supportedTasks: "Overview of Supported Deployment Tasks",
changeLog: "Change Log",
stableRelease: "Stable",
@@ -17,6 +17,15 @@ export default {
certNoExpireCount: "未过期",
noCert: "还没有证书",
manageCert: "查看证书",
siteMonitorCount: "监控站点数量",
siteAbnormalCount: "异常",
siteNormalCount: "正常",
manageSiteMonitor: "管理监控站点",
domainCount: "域名数量",
domainExpiringCount: "即将到期",
domainExpiredCount: "已过期",
domainNotExpiredCount: "未到期",
manageDomain: "管理域名",
createNow: "立即创建",
managePipeline: "管理流水线",
@@ -24,6 +33,16 @@ export default {
recentRun: "最近运行统计",
runCount: "运行次数",
expiringCerts: "最快到期证书",
totalGeneratedCertCount: "累计生成证书数量",
singleDomainCertShortCount: "单域名",
multiDomainCertShortCount: "多域名",
wildcardCertShortCount: "泛域名",
singleDomainCertCount: "累计单域名证书数量",
multiDomainCertCount: "累计多域名证书数量",
wildcardCertCount: "累计泛域名证书数量",
savedAmount: "已为您累计节省",
savedAmountRule: "按腾讯云证书平台一年期单价的 1/4 进行估算。实际节省金额仅供参考。(腾讯云单域名证书 532 元,多域名证书 912 元,泛域名证书 2147 元)",
totalPipelineRuns: "累计流水线运行次数",
supportedTasks: "已支持的部署任务总览",
changeLog: "更新日志",
stableRelease: "稳定版",
@@ -1,13 +1,54 @@
import { DynamicType, FormItemProps, useMerge } from "@fast-crud/fast-crud";
import { merge, set } from "lodash-es";
import { defineStore } from "pinia";
import * as api from "./api.plugin";
import { DynamicType, FormItemProps, useMerge } from "@fast-crud/fast-crud";
import { i18n } from "/src/locales/i18n";
import { cloneDeep } from "lodash-es";
interface PluginState {
group?: PluginGroups;
originGroup?: PluginGroups;
}
export function fillPluginDefine(plugin: any) {
const columns = plugin.input;
for (const key in columns) {
const column = columns[key];
if (!column.component) {
column.component = {
name: "a-input",
vModel: "value",
};
} else {
if (column.component.vModel == null && column.component.name?.startsWith("a-")) {
column.component.vModel = "value";
}
}
}
if (plugin.sysSetting?.metadata) {
merge(plugin.input, plugin.sysSetting.metadata?.input || {});
// 应用选项映射
for (const key of Object.keys(plugin.input)) {
const inputDef = plugin.input[key];
if (inputDef.optionsMapping && inputDef.component?.options) {
const mapping = inputDef.optionsMapping;
for (const opt of inputDef.component.options) {
if (mapping[opt.value] !== undefined) {
opt.label = mapping[opt.value];
}
}
}
}
}
// if (plugin.sysSetting?.input) {
// // 设置系统默认值, 不能放这里设置
// for (const key in plugin.input) {
// const sysValue = plugin.sysSetting?.input[key];
// if (sysValue != undefined) {
// set(plugin, `input.${key}.value`, sysValue);
// }
// }
// }
}
export type PluginGroup = {
key: string;
title: string;
@@ -28,6 +69,7 @@ export type PluginDefine = {
output: {
[key: string]: any;
};
type?: string;
};
export class PluginGroups {
@@ -35,10 +77,9 @@ export class PluginGroups {
map!: { [key: string]: PluginDefine };
t: any;
mergeSetting?: boolean;
constructor(groups: { [key: string]: PluginGroup }, opts?: { mergeSetting?: boolean }) {
constructor(groups: { [key: string]: PluginGroup }) {
this.groups = groups;
this.t = i18n.global.t;
this.mergeSetting = opts?.mergeSetting ?? false;
this.initGroup(groups);
this.initMap();
}
@@ -53,26 +94,9 @@ export class PluginGroups {
icon: "material-symbols:border-all-rounded",
};
for (const key in groups) {
if (this.mergeSetting) {
for (const plugin of groups[key].plugins) {
if (plugin.sysSetting) {
merge(plugin.input, plugin.sysSetting.metadata?.input || {});
// 应用选项映射
for (const key of Object.keys(plugin.input)) {
const inputDef = plugin.input[key];
if (inputDef.optionsMapping && inputDef.component?.options) {
const mapping = inputDef.optionsMapping;
for (const opt of inputDef.component.options) {
if (mapping[opt.value] !== undefined) {
opt.label = mapping[opt.value];
}
}
}
}
}
}
for (const plugin of groups[key].plugins) {
fillPluginDefine(plugin);
}
all.plugins.push(...groups[key].plugins);
}
this.groups = {
@@ -173,16 +197,12 @@ export const usePluginStore = defineStore({
id: "app.plugin",
state: (): PluginState => ({
group: null,
originGroup: null,
}),
actions: {
async reload() {
const groups = await api.GetGroups({});
const installedGroups = filterInstalledPluginGroups(groups);
this.group = new PluginGroups(installedGroups, { mergeSetting: true });
this.originGroup = new PluginGroups(cloneDeep(installedGroups));
this.group = new PluginGroups(groups);
console.log("group", this.group);
console.log("originGroup", this.originGroup);
},
async init() {
if (!this.group) {
@@ -206,10 +226,6 @@ export const usePluginStore = defineStore({
await this.init();
return this.group.get(name);
},
async getPluginDefineFromOrigin(name: string): Promise<PluginDefine> {
await this.init();
return this.originGroup.get(name);
},
async getPluginConfig(query: any) {
return await api.GetPluginConfig(query);
},
@@ -24,7 +24,8 @@ export const useProjectStore = defineStore("app.project", () => {
}
const userStore = useUserStore();
const userId = userStore.getUserInfo?.id;
const lastProjectIdCacheKey = "currentProjectId:" + userId;
const settingStore = useSettingStore();
const lastProjectIdCacheKey = `currentProjectId:${userId}`;
const lastProjectId = LocalStorage.get(lastProjectIdCacheKey);
currentProjectId.value = lastProjectId;
@@ -45,7 +46,6 @@ export const useProjectStore = defineStore("app.project", () => {
return null;
});
const settingStore = useSettingStore();
const isEnterprise = computed(() => {
return settingStore.isEnterprise;
});
@@ -390,6 +390,9 @@ export const useSettingStore = defineStore({
this.loadProductInfo();
this.inited = true;
},
getSiteId() {
return this.installInfo?.siteId || "";
},
},
});
@@ -280,6 +280,8 @@ button.ant-btn.isPlus {
align-items: center;
img {
width: 16px;
height: 16px;
&.font-size-16 {
width: 16px;
height: 16px;
@@ -3,7 +3,7 @@
<template #header>
<div class="title">
DNS持久验证记录
<span class="red sub" style="color: red">当前仅 Let's Encrypt 测试环境可以申请 DNS 持久验证证书。</span>
<span class="red sub" style="color: red">当前仅 Let's Encrypt 测试环境 可以申请 DNS 持久验证证书。</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
@@ -235,7 +235,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
} else if (value === "dns-persist") {
Modal.confirm({
title: "请前往DNS持久验证记录页面添加记录",
content: "DNS持久验证需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Let's Encrypt 测试环境可以申请。",
content: "DNS持久验证需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Let's Encrypt 测试环境 可以申请。",
async onOk() {
router.push({
path: "/cert/cert/dns-persist",
@@ -152,6 +152,13 @@ export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref
});
const initialForm = req.initialForm || {};
const pluginSysConfig = await pluginStore.getPluginConfig({ name: certPlugin.name, type: "builtIn" });
if (pluginSysConfig?.sysSetting?.input) {
//设置系统默认值,必须放这里设置,保证随时应用最新的系统值
for (const key in pluginSysConfig.sysSetting?.input) {
initialForm.input[key] = pluginSysConfig.sysSetting?.input[key];
}
}
initialForm.type = certPlugin.name;
const applyTemplates = reactive<any[]>([]);
const selectedTemplateId = ref<number | null>(null);
@@ -556,12 +563,12 @@ export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref
//设置系统初始值
const initialForm: any = { input: {} };
const pluginSysConfig = await pluginStore.getPluginConfig({ name: req.pluginName, type: "builtIn" });
if (pluginSysConfig.sysSetting?.input) {
for (const key in pluginSysConfig.sysSetting?.input) {
initialForm.input[key] = pluginSysConfig.sysSetting?.input[key];
}
}
// const pluginSysConfig = await pluginStore.getPluginConfig({ name: req.pluginName, type: "builtIn" });
// if (pluginSysConfig.sysSetting?.input) {
// for (const key in pluginSysConfig.sysSetting?.input) {
// initialForm.input[key] = pluginSysConfig.sysSetting?.input[key];
// }
// }
async function doSubmit({ form }: any) {
// const certDetail = readCertDetail(form.cert.crt);
// 添加certd pipeline
@@ -263,8 +263,8 @@ function useStepForm() {
currentStep.input[key] = column.default ?? column.value;
}
}
//设置系统初始值
const pluginSysConfig = await pluginStore.getPluginConfig({ name: pluginDefine.name, type: "builtIn" });
//设置系统初始值,必须放在这里设置,保证最新的系统值被清空后,这里也能跟着清空
const pluginSysConfig = await pluginStore.getPluginConfig({ name: pluginDefine.name, type: pluginDefine.type });
if (pluginSysConfig.sysSetting?.input) {
for (const key in pluginSysConfig.sysSetting?.input) {
currentStep.input[key] = pluginSysConfig.sysSetting?.input[key];
@@ -8,7 +8,8 @@ import { CanvasRenderer } from "echarts/renderers";
import { PieChart } from "echarts/charts";
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from "echarts/components";
import VChart, { THEME_KEY } from "vue-echarts";
import { ref, provide, defineProps } from "vue";
import { computed, provide, defineProps } from "vue";
import { usePreferences } from "/@/vben/preferences";
import { ChartItem } from "./d";
use([CanvasRenderer, PieChart, TitleComponent, TooltipComponent, GridComponent, LegendComponent]);
@@ -18,16 +19,26 @@ provide(THEME_KEY, "");
const props = defineProps<{
data: ChartItem[];
}>();
const { isDark } = usePreferences();
const option = ref({
const option = computed(() => ({
color: ["#91cc75", "#73c0de", "#ee6666", "#fac858", "#5470c6", "#3ba272", "#fc8452", "#9a60b4", "#ea7ccc", "#5470c6"],
tooltip: {
trigger: "item",
backgroundColor: isDark.value ? "rgba(35, 38, 45, 0.96)" : "rgba(255, 255, 255, 0.96)",
borderColor: isDark.value ? "rgba(255, 255, 255, 0.2)" : "#d9d9d9",
textStyle: {
color: isDark.value ? "rgba(242, 242, 242, 0.92)" : "#333",
},
},
legend: {
orient: "vertical",
bottom: "5%",
left: "left",
textStyle: {
color: isDark.value ? "rgba(242, 242, 242, 0.85)" : "#555",
},
inactiveColor: isDark.value ? "rgba(242, 242, 242, 0.35)" : "#ccc",
},
grid: {
top: "20px",
@@ -65,7 +76,7 @@ const option = ref({
data: props.data,
},
],
});
}));
</script>
<style lang="less">
@@ -65,7 +65,7 @@
<SimpleSteps></SimpleSteps>
</div>
</div>
<div class="warning">
<div v-if="noticeList.length > 0" class="warning">
<notice-bar :list="noticeList"></notice-bar>
</div>
@@ -84,11 +84,22 @@
</template>
</statistic-card>
</a-col>
<!-- <a-col :md="6" :xs="24">
<a-col :md="6" :xs="24">
<statistic-card :title="t('certd.dashboard.pipelineStatus')" :footer="false">
<pie-count v-if="count.pipelineStatusCount" :data="count.pipelineStatusCount"></pie-count>
<pie-count v-if="count.pipelineStatusCount" :data="count.pipelineStatusCount" />
</statistic-card>
</a-col> -->
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:data-trending-24" :title="t('certd.dashboard.recentRun')" :footer="false">
<day-count v-if="count.historyCountPerDay" :data="count.historyCountPerDay" :title="t('certd.dashboard.runCount')"></day-count>
</statistic-card>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:alert-urgent-24" :title="t('certd.dashboard.expiringCerts')">
<expiring-list v-if="count.expiringList" :data="count.expiringList"></expiring-list>
</statistic-card>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:certificate-24" :title="t('certd.dashboard.certCount')" :count="count.certCount" link="/cert/monitor/cert" :sub-counts="count.certStatusCount">
<template v-if="count.certCount === 0" #default>
@@ -102,13 +113,29 @@
</statistic-card>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:data-trending-24" :title="t('certd.dashboard.recentRun')" :footer="false">
<day-count v-if="count.historyCountPerDay" :data="count.historyCountPerDay" :title="t('certd.dashboard.runCount')"></day-count>
<statistic-card
icon="fluent-color:certificate-24"
:title="t('certd.dashboard.totalGeneratedCertCount')"
:count="count.totalGeneratedCertCount"
:sub-counts="count.generatedCertTypeCounts"
:helper-text="t('certd.dashboard.savedAmount')"
:helper-value="`¥${formatSavedAmount(count.totalSavedAmount)}`"
:helper-tooltip="t('certd.dashboard.savedAmountRule')"
:footer="false"
/>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:link-multiple-24" :title="t('certd.dashboard.siteMonitorCount')" :count="count.siteCount" link="/cert/monitor/site" :sub-counts="count.siteStatusCount">
<template #footer>
<router-link to="/cert/monitor/site" class="flex"> <fs-icon icon="ion:settings-outline" class="mr-5 fs-16" /> {{ t("certd.dashboard.manageSiteMonitor") }} </router-link>
</template>
</statistic-card>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card icon="fluent-color:alert-urgent-24" :title="t('certd.dashboard.expiringCerts')">
<expiring-list v-if="count.expiringList" :data="count.expiringList"></expiring-list>
<statistic-card icon="fluent-color:globe-24" :title="t('certd.dashboard.domainCount')" :count="count.domainCount" link="/cert/cert/domain" :sub-counts="count.domainStatusCount">
<template #footer>
<router-link to="/cert/cert/domain" class="flex"> <fs-icon icon="ion:settings-outline" class="mr-5 fs-16" /> {{ t("certd.dashboard.manageDomain") }} </router-link>
</template>
</statistic-card>
</a-col>
</a-row>
@@ -121,6 +148,7 @@
<fs-icon icon="fluent-color:puzzle-piece-24" class="mr-5 fs-28" />
<div class="mr-5">{{ t("certd.dashboard.supportedTasks") }}</div>
<a-tag color="green">{{ pluginGroups.groups.all.plugins.length }}</a-tag>
<div class="helper">列表仅做展示,请创建流水线来使用它们</div>
</div>
</template>
<a-row :gutter="10">
@@ -159,6 +187,7 @@ import { useRouter } from "vue-router";
import * as api from "./api";
import DayCount from "./charts/day-count.vue";
import ExpiringList from "./charts/expiring-list.vue";
import PieCount from "./charts/pie-count.vue";
import NoticeBar from "./notice-bar.vue";
import SuiteCard from "./suite-card.vue";
import TutorialButton from "/@/components/tutorial/index.vue";
@@ -260,6 +289,10 @@ function goPipeline() {
}
const count: any = ref({});
function formatSavedAmount(amountValue: number) {
const amount = Number(amountValue) || 0;
return Number.isInteger(amount) ? amount.toString() : amount.toFixed(2);
}
function transformStatusCount() {
const data = count.value.pipelineStatusCount;
const sorted = [
@@ -300,6 +333,33 @@ function transformStatusCount() {
{ name: t("certd.dashboard.certNoExpireCount"), value: certCount.notExpired, color: "green", link: { path: "/cert/monitor/cert", query: { expireStatus: "noExpired" } } },
];
count.value.certCount = certCount.total;
const siteCount = count.value.siteCount;
count.value.siteStatusCount = [
{ name: t("certd.dashboard.siteAbnormalCount"), value: siteCount.abnormal, color: "red", checkIcon: "mingcute:warning-fill:#f44336", link: { path: "/cert/monitor/site", query: { checkStatus: "error" } } },
{ name: t("certd.dashboard.siteNormalCount"), value: siteCount.normal, color: "green" },
];
count.value.siteCount = siteCount.total;
const domainCount = count.value.domainCount;
count.value.domainStatusCount = [
{ name: t("certd.dashboard.domainExpiredCount"), value: domainCount.expired, color: "red", checkIcon: "mingcute:warning-fill:#f44336", link: { path: "/cert/cert/domain", query: { expirationStatus: "expired" } } },
{ name: t("certd.dashboard.domainExpiringCount"), value: domainCount.expiring, color: "yellow", checkIcon: "mingcute:alert-fill:#ff9800" },
{ name: t("certd.dashboard.domainNotExpiredCount"), value: domainCount.notExpired, color: "green" },
];
count.value.domainCount = domainCount.total;
const genCertCount = count.value.genCertCount || {};
const singleDomainCertCount = Number(genCertCount.singleDomainCertCount) || 0;
const multiDomainCertCount = Number(genCertCount.multiDomainCertCount) || 0;
const wildcardCertCount = Number(genCertCount.wildcardCertCount) || 0;
count.value.totalGeneratedCertCount = singleDomainCertCount + multiDomainCertCount + wildcardCertCount;
count.value.generatedCertTypeCounts = [
{ name: t("certd.dashboard.singleDomainCertShortCount"), value: singleDomainCertCount, color: "blue" },
{ name: t("certd.dashboard.multiDomainCertShortCount"), value: multiDomainCertCount, color: "blue" },
{ name: t("certd.dashboard.wildcardCertShortCount"), value: wildcardCertCount, color: "blue" },
];
count.value.totalSavedAmount = singleDomainCertCount * (532 / 4) + multiDomainCertCount * (912 / 4) + wildcardCertCount * (2147 / 4);
}
async function loadCount() {
count.value = await GetStatisticCount();
@@ -1,6 +1,6 @@
<template>
<div class="statistic-card">
<a-card>
<a-card :class="cardTone">
<div class="data-item">
<div class="header">
<div class="title">
@@ -11,9 +11,9 @@
</div>
<div class="content">
<div v-if="!slots.default" class="statistic">
<div v-if="count !== 0" class="value flex items-center w-full">
<div class="value flex items-center w-full">
<div class="total flex-center flex-1 flex-col pointer" @click="goDetail(link)">
<span>{{ count }}</span>
<span>{{ count ?? 0 }}</span>
<span class="sub-title">{{ title }}</span>
</div>
<a-divider type="vertical h-10"></a-divider>
@@ -32,7 +32,13 @@
</div>
</div>
</div>
<a-empty v-else></a-empty>
<div v-if="helperText" class="helper-text mt-10">
<span>{{ helperText }}</span>
<strong v-if="helperValue" class="helper-value">{{ helperValue }}</strong>
<a-tooltip v-if="helperTooltip" :title="helperTooltip">
<fs-icon icon="mingcute:information-line" class="helper-icon" />
</a-tooltip>
</div>
</div>
<slot></slot>
</div>
@@ -45,6 +51,7 @@
</template>
<script setup lang="ts">
import { FsIcon } from "@fast-crud/fast-crud";
import { computed } from "vue";
import { useRouter } from "vue-router";
const props = defineProps<{
icon: string;
@@ -59,9 +66,21 @@ const props = defineProps<{
title?: string;
link?: any;
}[];
helperText?: string;
helperValue?: string;
helperTooltip?: string;
}>();
const slots = defineSlots();
const router = useRouter();
const cardTone = computed(() => {
if (props.subCounts?.some(item => item.color === "red" && item.value > 0)) {
return "has-danger";
}
if (props.subCounts?.some(item => item.color === "yellow" && item.value > 0)) {
return "has-warning";
}
return "";
});
function goDetail(link: any) {
if (!link) {
return;
@@ -80,9 +99,35 @@ function goDetail(link: any) {
color: rgba(242, 242, 242, 0.85) !important;
}
}
.statistic-card {
.ant-card.has-danger {
background: linear-gradient(135deg, rgba(92, 35, 35, 0.72), rgba(28, 28, 28, 0.96) 62%);
}
.ant-card.has-warning {
background: linear-gradient(135deg, rgba(91, 72, 25, 0.68), rgba(28, 28, 28, 0.96) 62%);
}
.helper-text {
color: rgba(242, 242, 242, 0.72);
.helper-value {
color: #8bd7a0;
}
}
}
}
.statistic-card {
margin-bottom: 10px;
.ant-card.has-danger {
background: linear-gradient(135deg, rgba(255, 241, 240, 0.9), #fff 58%);
}
.ant-card.has-warning {
background: linear-gradient(135deg, rgba(255, 251, 230, 0.9), #fff 58%);
}
.icon-text {
display: inline-flex;
justify-content: left;
@@ -143,6 +188,7 @@ function goDetail(link: any) {
font-weight: 700;
.total {
color: hsl(var(--primary));
.sub-title {
font-size: 12px;
font-weight: 400;
@@ -180,6 +226,31 @@ function goDetail(link: any) {
}
}
}
.helper-text {
display: flex;
align-items: baseline;
justify-content: center;
gap: 4px;
margin-top: 6px;
color: #626262;
font-size: 12px;
font-weight: 400;
line-height: 20px;
.helper-value {
color: #52a26a;
font-size: 16px;
font-weight: 700;
}
.helper-icon {
cursor: help;
font-size: 14px;
margin-left: 2px;
vertical-align: -2px;
}
}
}
}
@@ -1,3 +1,4 @@
import { PluginDefine } from "@certd/pipeline";
import { request } from "/src/api/service";
const apiPrefix = "/sys/plugin";
@@ -108,6 +109,7 @@ export type OnlinePluginBean = {
desc?: string;
latest?: string;
status?: string;
email?: string;
downloadCount?: number;
score?: number;
aiCheckStatus?: string;
@@ -218,14 +220,8 @@ export async function OnlinePluginPublishInfo(body: { id: number }): Promise<{
export type OnlinePluginAuthorBean = {
id?: number;
appId?: number;
appOwnerId?: number;
developerId?: number;
name?: string;
displayName?: string;
avatar?: string;
desc?: string;
status?: string;
email?: string;
};
export async function OnlinePluginAuthorGet(options?: { showErrorNotify?: boolean }): Promise<{ registered?: boolean; author?: OnlinePluginAuthorBean }> {
@@ -236,18 +232,22 @@ export async function OnlinePluginAuthorGet(options?: { showErrorNotify?: boolea
});
}
export async function OnlinePluginAuthorAdd(body: { name: string; displayName?: string; avatar?: string; desc?: string }): Promise<OnlinePluginAuthorBean> {
export async function OnlinePluginAuthorAdd(body: { name: string; email: string }): Promise<OnlinePluginAuthorBean> {
return await request({
url: apiPrefix + "/online/author/add",
method: "post",
data: body,
});
}
export async function OnlinePluginAuthorUpdate(body: { email: string }): Promise<OnlinePluginAuthorBean> {
return await request({ url: apiPrefix + "/online/author/update", method: "post", data: body });
}
export type PluginConfigBean = {
name: string;
disabled: boolean;
sysSetting: {
metadata?: Record<string, any>;
input?: Record<string, any>;
};
};
@@ -285,7 +285,7 @@ export async function SaveCommPluginConfigs(data: CommPluginConfig): Promise<voi
});
}
export async function savePluginSetting(req: { name: string; sysSetting: any }): Promise<void> {
export async function savePluginSetting(req: { name: string; sysSetting: any; type: string }): Promise<void> {
return await request({
url: apiPrefix + "/saveSetting",
method: "post",
@@ -293,11 +293,11 @@ export async function savePluginSetting(req: { name: string; sysSetting: any }):
});
}
export async function GetPluginByName(name: string): Promise<PluginConfigBean> {
export async function getPluginSetting(name: string, type: string = "builtIn"): Promise<PluginConfigBean> {
return await request({
url: apiPrefix + "/getPluginByName",
url: apiPrefix + "/getSetting",
method: "post",
data: { name },
data: { name, type },
});
}
@@ -307,3 +307,11 @@ export async function ClearRuntimeDeps(): Promise<void> {
method: "post",
});
}
export async function getPluginDefine(name: string): Promise<PluginDefine> {
return await request({
url: apiPrefix + "/getPluginDefine",
method: "post",
data: { name },
});
}
@@ -1,7 +1,9 @@
<template>
<div class="plugin-author-field">
<a-input :value="modelValue" placeholder="请输入作者名称" @update:value="emit('update:modelValue', $event)" />
<a-button type="link" size="small" :loading="loading" @click="registerAuthor">注册作者</a-button>
<a-input :value="modelValue" placeholder="请输入作者名称" :disabled="authorRegistered" @update:value="emit('update:modelValue', $event)" />
<a-button type="link" size="small" :loading="loading" @click="handleAuthorAction">
{{ authorRegistered ? "修改邮箱" : "注册作者" }}
</a-button>
</div>
</template>
@@ -15,7 +17,9 @@ defineOptions({ name: "PluginAuthorField" });
const props = defineProps<{ modelValue?: string }>();
const emit = defineEmits<{ (event: "update:modelValue", value: string): void }>();
const loading = ref(false);
const { registerPluginAuthor } = usePluginPublish();
const { registerPluginAuthor, updatePluginAuthorEmail } = usePluginPublish();
const registeredAuthor = ref<api.OnlinePluginAuthorBean>();
const authorRegistered = computed(() => !!registeredAuthor.value?.id);
const isLocalAuthor = computed(() => {
return (
String(props.modelValue || "")
@@ -28,7 +32,8 @@ async function loadAuthor() {
loading.value = true;
try {
const result = await api.OnlinePluginAuthorGet({ showErrorNotify: false });
if ((!props.modelValue || isLocalAuthor.value) && result.author?.name) {
if (result.registered && result.author?.id && result.author.name) {
registeredAuthor.value = result.author;
emit("update:modelValue", result.author.name);
}
} catch {
@@ -38,11 +43,19 @@ async function loadAuthor() {
}
}
async function registerAuthor() {
async function handleAuthorAction() {
if (registeredAuthor.value) {
const author = await updatePluginAuthorEmail(registeredAuthor.value);
if (author?.id) {
registeredAuthor.value = author;
}
return;
}
const author = await registerPluginAuthor();
if (!author?.name) {
return;
}
registeredAuthor.value = author;
emit("update:modelValue", author.name);
}
@@ -105,6 +105,9 @@ async function loadPlugin() {
if (baseForm.extra) {
baseForm.extra = yaml.load(baseForm.extra);
}
if (baseForm.dependPlugins) {
baseForm.dependPlugins = yaml.load(baseForm.dependPlugins);
}
delete baseForm.metadata;
delete baseForm.content;
baseFormRef.value.setFormData(baseForm);
@@ -136,6 +139,9 @@ function buildSubmitForm(versionOverride?: string) {
if (form.extra) {
form.extra = yaml.dump(form.extra);
}
if (form.dependPlugins) {
form.dependPlugins = yaml.dump(form.dependPlugins);
}
return form;
}
@@ -29,7 +29,7 @@
</div>
</div>
<div class="plugin-card__actions">
<div v-if="pluginCardState.showEditButton || pluginCardState.showConfigButton" class="plugin-card__tools">
<div v-if="pluginCardState.showEditButton || pluginCardState.showConfigButton || pluginCardState.canExportPlugin" class="plugin-card__tools">
<a-tooltip v-if="pluginCardState.showEditButton" title="编辑">
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="editPlugin">
<template #icon>
@@ -37,10 +37,10 @@
</template>
</a-button>
</a-tooltip>
<a-tooltip v-if="copyHandler && pluginCardState.canCopyPlugin" title="复制">
<a-button class="plugin-card__tool" type="text" size="small" @click.stop="copyPlugin">
<a-tooltip v-if="pluginCardState.canExportPlugin" :title="t('certd.export')">
<a-button class="plugin-card__tool" type="text" size="small" :loading="isActionLoading('export')" @click.stop="exportPlugin">
<template #icon>
<fs-icon icon="ion:copy-outline" />
<fs-icon icon="ion:cloud-download-outline" />
</template>
</a-button>
</a-tooltip>
@@ -89,7 +89,7 @@
<div class="plugin-card__meta">
<template v-if="source === 'local'">
<a-tooltip :title="toggleTitle">
<a-tooltip v-if="plugin.showDisabled" :title="toggleTitle">
<a-tag class="plugin-status-tag" :class="{ 'is-loading': isActionLoading('toggle') }" :color="plugin.disabled ? 'default' : 'green'" @click.stop="togglePlugin">
{{ plugin.disabled ? t("certd.onlinePluginDisabled") : t("certd.onlinePluginEnabled") }}
</a-tag>
@@ -99,7 +99,7 @@
<a-tag v-if="plugin.group">{{ plugin.group }}</a-tag>
</template>
<template v-else-if="!simple">
<a-tooltip v-if="plugin.installed && plugin.id" :title="toggleTitle">
<a-tooltip v-if="plugin.installed && plugin.id && plugin.showDisabled" :title="toggleTitle">
<a-tag class="plugin-status-tag" :class="{ 'is-loading': isActionLoading('toggle') }" :color="plugin.disabled ? 'default' : 'green'" @click.stop="togglePlugin">
{{ plugin.disabled ? t("certd.onlinePluginDisabled") : t("certd.onlinePluginEnabled") }}
</a-tag>
@@ -147,6 +147,7 @@ import PluginEditDialogBody from "./plugin-edit-dialog-body.vue";
import { usePluginStore } from "/@/store/plugin";
import { useFormDialog } from "/@/use/use-dialog";
import { useI18n } from "/src/locales";
import { useSettingStore } from "/@/store/settings";
defineOptions({
name: "PluginItemCard",
@@ -160,11 +161,9 @@ const props = withDefaults(
simple?: boolean;
showConfig?: boolean;
editable?: boolean;
copyHandler?: (plugin: any) => void | Promise<void>;
}>(),
{
source: "market",
copyHandler: undefined,
}
);
@@ -183,8 +182,9 @@ const { resolveOnlinePluginDependencies, installOnlinePluginChain, openDependenc
const actionLoading = ref<PluginCardAction | "">("");
const editDialogBodyRef = ref();
type PluginCardAction = "edit" | "copy" | "publish" | "config" | "install" | "uninstall" | "remove" | "toggle";
type PluginCardAction = "edit" | "export" | "publish" | "config" | "install" | "uninstall" | "remove" | "toggle";
const settingStore = useSettingStore();
const pluginCardState = computed(() => {
const isLocal = props.source === "local";
const isCloudPlugin = Number(props.plugin.developerId) > 0;
@@ -192,7 +192,6 @@ const pluginCardState = computed(() => {
const isAvailableLocally = isLocal || !!props.plugin.installed;
const canEditPlugin = props.editable === true;
const canManagePlugin = !isBuiltInPlugin && canEditPlugin && isAvailableLocally;
return {
editPluginId: props.plugin.id,
isLocal,
@@ -200,11 +199,12 @@ const pluginCardState = computed(() => {
canEditPlugin,
isAvailableLocally,
canManagePlugin,
canCopyPlugin: canManagePlugin,
canExportPlugin: canManagePlugin,
showEditButton: canManagePlugin,
showConfigButton: !!props.showConfig,
showConfigButton: !!props.showConfig && isAvailableLocally && settingStore.isComm,
isInstalled: isCloudPlugin && !!props.plugin.installed,
isDisabled: !!props.plugin.disabled,
showDisabled: settingStore.isComm,
requiresVip: props.plugin.vip === "plus" || props.plugin.needPlus === true,
canRemovePlugin: isLocal && canManagePlugin,
isBuiltInPlugin,
@@ -354,16 +354,26 @@ async function editPlugin() {
});
}
async function copyPlugin() {
if (!props.copyHandler) {
async function exportPlugin() {
if (!pluginCardState.value.canExportPlugin || !props.plugin.id) {
return;
}
await runAction(
"copy",
"export",
async () => {
await props.copyHandler?.(props.plugin);
const content = await api.ExportPlugin(props.plugin.id);
if (!content) {
return;
}
const blob = new Blob([content], { type: "text/yaml;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${props.plugin.name || props.plugin.fullName || "plugin"}.yaml`;
link.click();
URL.revokeObjectURL(url);
},
{ emitChanged: true }
{ emitChanged: false }
);
}
@@ -1052,5 +1062,4 @@ function handleVersionClick() {
}
}
}
</style>
@@ -69,7 +69,7 @@
<script lang="ts" setup>
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import { reactive, ref } from "vue";
import { CommPluginConfig, GetCommPluginConfigs, SaveCommPluginConfigs, GetPluginByName } from "/@/views/sys/plugin/api";
import { CommPluginConfig, GetCommPluginConfigs, SaveCommPluginConfigs, getPluginSetting } from "/@/views/sys/plugin/api";
import { merge } from "lodash-es";
import { notification } from "ant-design-vue";
import { usePluginConfig } from "./use-config";
@@ -113,7 +113,7 @@ const onFinishFailed = (errorInfo: any) => {
const { openConfigDialog } = usePluginConfig();
async function doPluginConfig() {
const certApplyInfo = await GetPluginByName("CertApply");
const certApplyInfo = await getPluginSetting("CertApply");
await openConfigDialog({
row: certApplyInfo,
onSuccess: async () => {
@@ -48,7 +48,7 @@ import { cloneDeep, merge, unset } from "lodash-es";
import { computed, onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import Rollbackable from "./rollbackable.vue";
import { usePluginStore } from "/@/store/plugin";
import { fillPluginDefine, usePluginStore } from "/@/store/plugin";
import * as api from "./api";
// @ts-ignore js-yaml 没有在当前前端包中提供类型声明。
import yaml from "js-yaml";
@@ -106,7 +106,7 @@ const editableKeys = ref([
},
editRender(item: any) {
return () => {
return <fs-component-render {...item.component} vModel:modelValue={configForm[item.key]["value"]} scope={getScope()} />;
return <fs-component-render {...item.component} vModel:modelValue={configForm[item.key].value} scope={getScope()} />;
};
},
},
@@ -267,38 +267,18 @@ function clearFormValue(key: string) {
console.log(key, configForm);
}
function getPluginOriginName() {
if (props.plugin.fullName) {
return props.plugin.fullName;
}
if (props.plugin.author && props.plugin.name) {
return `${props.plugin.author}/${props.plugin.name}`;
}
return props.plugin.name;
}
async function loadPluginSetting() {
const originName = getPluginOriginName();
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(originName);
if (!currentPlugin.value?.input && originName !== props.plugin.name) {
currentPlugin.value = await pluginStore.getPluginDefineFromOrigin(props.plugin.name);
}
if (!currentPlugin.value?.input && props.plugin.id) {
const plugin = await api.GetObj(props.plugin.id);
const metadata = plugin.metadata ? yaml.load(plugin.metadata) : {};
currentPlugin.value = {
...plugin,
...(metadata || {}),
};
}
const pluginName = props.plugin.fullName || props.plugin.name; // 这里必须两种进行兼容
currentPlugin.value = await api.getPluginDefine(pluginName);
fillPluginDefine(currentPlugin.value);
const input = currentPlugin.value?.input || {};
for (const key in input) {
configForm[key] = {};
}
const setting = currentPlugin.value?.sysSetting || props.plugin.sysSetting;
if (setting) {
const settingJson = JSON.parse(setting);
merge(configForm, settingJson.metadata?.input || {});
const pluginConfig = await api.getPluginSetting(pluginName, currentPlugin.value.type);
if (pluginConfig?.sysSetting) {
merge(configForm, pluginConfig.sysSetting?.metadata?.input || {});
}
}
@@ -383,7 +383,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
{ label: t("certd.dns"), value: "dnsProvider" },
{ label: t("certd.deployPlugin"), value: "deploy" },
{ label: "通知", value: "notification" },
{ label: "Addon", value: "addon" },
// { label: "Addon", value: "addon" },
],
}),
column: {
@@ -463,6 +463,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
name: PluginAuthorField,
vModel: "modelValue",
},
helper: "为保证插件名称引用一致性,作者一旦注册不允许修改",
rules: [{ required: true, message: "请先注册并选择插件作者" }],
},
column: {
@@ -613,6 +614,16 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
column: {
show: false,
},
valueBuilder({ row }) {
if (typeof row.dependPlugins === "string") {
row.dependPlugins = yaml.load(row.dependPlugins);
}
},
valueResolve({ row }) {
if (row.dependPlugins && typeof row.dependPlugins === "object") {
row.dependPlugins = yaml.dump(row.dependPlugins);
}
},
},
"extra.dependPackages": {
title: t("certd.thirdPartyDependencies"),
@@ -9,17 +9,7 @@
<fs-crud ref="crudRef" class="plugin-card-crud" v-bind="crudBinding">
<a-empty v-if="pluginList.length === 0" class="plugin-card-empty" />
<div v-else class="plugin-card-grid">
<PluginItemCard
v-for="item of pluginList"
:key="item.fullName"
:source="getPluginCardSource(item)"
:plugin="item"
show-config
:editable="item.editable"
:copy-handler="copyPlugin"
@changed="handlePluginChanged"
@click="openPluginDetail"
/>
<PluginItemCard v-for="item of pluginList" :key="item.fullName" :source="getPluginCardSource(item)" :plugin="item" show-config :editable="item.editable" @changed="handlePluginChanged" @click="openPluginDetail" />
</div>
</fs-crud>
</fs-page>
@@ -80,30 +70,8 @@ async function handleDetailInstalled() {
crudExpose.doRefresh();
}
async function copyPlugin(row: any) {
const copyRow = { ...row };
delete copyRow.fullName;
delete copyRow.id;
delete copyRow.developerId;
delete copyRow.appId;
delete copyRow.latest;
delete copyRow.status;
delete copyRow.downloadCount;
delete copyRow.score;
await crudExpose.openCopy(
{
row: copyRow,
},
{
async onSuccess() {
crudExpose.doRefresh();
},
}
);
}
async function handlePluginChanged(payload: { action: string }) {
if (payload.action === "install" || payload.action === "uninstall" || payload.action === "remove" || payload.action === "copy") {
if (payload.action === "install" || payload.action === "uninstall" || payload.action === "remove") {
await pluginStore.reload();
}
crudExpose.doRefresh();
@@ -49,12 +49,13 @@ export function usePluginConfig() {
}
}
const res = await api.savePluginSetting({
name: row.name,
name: row.fullName,
sysSetting: {
metadata: {
input: newForm,
},
},
type: row.type,
});
await pluginStore.clear();
return res;
@@ -14,6 +14,9 @@ type PublishManagerResult =
| {
action: "registered";
}
| {
action: "updated";
}
| {
action: "cancel";
};
@@ -61,7 +64,7 @@ export function usePluginPublish() {
initialForm: {
name: "",
nameConfirm: "",
desc: "",
email: "",
},
async onSubmit(form: any) {
if (form.name?.trim() !== form.nameConfirm?.trim()) {
@@ -72,8 +75,7 @@ export function usePluginPublish() {
}
const author = await api.OnlinePluginAuthorAdd({
name: form.name,
displayName: form.displayName,
desc: form.desc,
email: form.email,
});
resolveAuthor(author);
},
@@ -111,6 +113,51 @@ export function usePluginPublish() {
],
},
},
email: {
title: "作者邮箱",
type: "text",
form: {
col: { span: 24 },
rules: [
{ required: true, message: "请输入作者邮箱" },
{ type: "email", message: "请输入有效的作者邮箱" },
],
},
},
},
});
});
}
function updatePluginAuthorEmail(author?: api.OnlinePluginAuthorBean) {
return new Promise<api.OnlinePluginAuthorBean | undefined>(async resolve => {
let resolved = false;
const resolveAuthor = (result?: api.OnlinePluginAuthorBean) => {
if (!resolved) {
resolved = true;
resolve(result);
}
};
await openFormDialog({
title: "修改作者邮箱",
wrapper: { width: 560, onClosed: () => resolveAuthor() },
initialForm: { email: author?.email || "" },
async onSubmit(form: any) {
const result = await api.OnlinePluginAuthorUpdate({ email: form.email });
resolveAuthor(result);
},
columns: {
email: {
title: "作者邮箱",
type: "text",
form: {
col: { span: 24 },
rules: [
{ required: true, message: "请输入作者邮箱" },
{ type: "email", message: "请输入有效的作者邮箱" },
],
},
},
},
});
});
@@ -326,7 +373,26 @@ export function usePluginPublish() {
function renderAuthor() {
if (info.authorRegistered) {
return author?.displayName || author?.name || "-";
return (
<span class="sys-plugin-publish__author-register">
<span>{author?.name || "-"}</span>
<a-button
size="small"
type="link"
onClick={async (event: any) => {
event.stopPropagation();
const updatedAuthor = await updatePluginAuthorEmail(author);
if (!updatedAuthor?.id) {
return;
}
resolveResult({ action: "updated" });
formWrapper?.close?.();
}}
>
修改邮箱
</a-button>
</span>
);
}
return (
<span class="sys-plugin-publish__author-register">
@@ -490,6 +556,10 @@ export function usePluginPublish() {
await publishLocalPlugin(row, options);
return;
}
if (action.action === "updated") {
await publishLocalPlugin(row, options);
return;
}
if (action.action !== "publish") {
return;
}
@@ -521,10 +591,10 @@ export function usePluginPublish() {
publishingPluginId.value = "";
}
}
return {
isPublishingPlugin,
publishLocalPlugin,
registerPluginAuthor,
updatePluginAuthorEmail,
};
}
+21
View File
@@ -3,6 +3,27 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
### Bug Fixes
* 修复插件市场插件安装无效的bug ([15d2bd8](https://github.com/certd/certd/commit/15d2bd8484a9ddd142bf4ef03d839c74405f9e6d))
* preserve Route53 TXT record values ([#791](https://github.com/certd/certd/issues/791)) ([f5430a8](https://github.com/certd/certd/commit/f5430a8925cf6cf4e16c75f2358fbd78437e6705))
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
### Bug Fixes
* 修复编辑证书申请任务导致清空ACME账号的bug ([d67379f](https://github.com/certd/certd/commit/d67379f29725bbd6dcdefe97d101023c7fd11f03))
* 修复大量问题 ([f911e06](https://github.com/certd/certd/commit/f911e06a698d568ae228880ec89b7e25fdd1305b))
* 修复旧流水线保存时清空证书仓库证书内容的bug ([f58f2f3](https://github.com/certd/certd/commit/f58f2f3cd603c37e55ac5868fdd2ca99aa937e44))
* **pipeline-controller:** 处理非编辑场景下保留id的问题 ([6606f32](https://github.com/certd/certd/commit/6606f3267868e3aadaca0092ef4c83198312553c))
* **pipeline:** 修正流水线保存时的ID判断逻辑并增加参数校验 ([6af12d4](https://github.com/certd/certd/commit/6af12d49fce1144943dc37ba574b3c9356998dcd))
### Performance Improvements
* **dashboard:** 新增仪表盘站点监控和域名管理的统计卡片 ([9842a24](https://github.com/certd/certd/commit/9842a24a50a841f710ee7c8050a38e3334c58394))
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
### Bug Fixes
@@ -14,6 +14,7 @@ input:
action: onCaTypeList
single: true
mode: tags
emitImmediate: true
required: true
mergeScript: |2-
@@ -90,7 +90,7 @@ input:
5. <b>自动匹配</b>:此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式
6. <b>DNS持久验证</b>:需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅
Let's Encrypt 测试环境可以申请
Let's Encrypt 测试环境 可以申请
order: 0
dnsProviderType:
title: DNS解析服务商
@@ -180,7 +180,7 @@ input:
}else if (form.challengeType === 'dnses'){
return '给每个域名单独配置dns提供商'
}else if (form.challengeType === 'dns-persist'){
return '请先创建并校验_validation-persist TXT持久记录,校验成功后才能提交流水线;当前仅 Let\'s Encrypt 测试环境可以申请'
return '请先创建并校验_validation-persist TXT持久记录,校验成功后才能提交流水线;当前仅 Let\'s Encrypt 测试环境 可以申请'
}
})
}
@@ -207,7 +207,7 @@ input:
return {
component:{
on: {
"selected-change": (scope)=>{
"selectedChange": (scope)=>{
// 切换颁发机构后,清空已选择的ACME账号,避免账号与颁发机构不匹配
let form = scope.form || {};
form = form.input || form.body || form;
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.44.0",
"version": "1.44.2",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -43,20 +43,20 @@
"lint1": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/acme-client": "^1.44.0",
"@certd/basic": "^1.44.0",
"@certd/commercial-core": "^1.44.0",
"@certd/acme-client": "^1.44.2",
"@certd/basic": "^1.44.2",
"@certd/commercial-core": "^1.44.2",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.44.0",
"@certd/lib-huawei": "^1.44.0",
"@certd/lib-k8s": "^1.44.0",
"@certd/lib-server": "^1.44.0",
"@certd/midway-flyway-js": "^1.44.0",
"@certd/pipeline": "^1.44.0",
"@certd/plugin-cert": "^1.44.0",
"@certd/plugin-lib": "^1.44.0",
"@certd/plugin-plus": "^1.44.0",
"@certd/plus-core": "^1.44.0",
"@certd/jdcloud": "^1.44.2",
"@certd/lib-huawei": "^1.44.2",
"@certd/lib-k8s": "^1.44.2",
"@certd/lib-server": "^1.44.2",
"@certd/midway-flyway-js": "^1.44.2",
"@certd/pipeline": "^1.44.2",
"@certd/plugin-cert": "^1.44.2",
"@certd/plugin-lib": "^1.44.2",
"@certd/plugin-plus": "^1.44.2",
"@certd/plus-core": "^1.44.2",
"@koa/cors": "^5.0.0",
"@midwayjs/bootstrap": "3.20.11",
"@midwayjs/cache": "3.14.0",
@@ -128,7 +128,6 @@ export class MainConfiguration {
ctx.response.set("Cache-Control", "public,max-age=0");
}
});
//acme setlogger
setLogger((text: string) => {
logger.info(text);
@@ -3,6 +3,7 @@ import { merge } from "lodash-es";
import { CrudController } from "@certd/lib-server";
import {
OnlinePluginAuthorAddReq,
OnlinePluginAuthorUpdateReq,
OnlinePluginDependenciesReq,
OnlinePluginInstallReq,
OnlinePluginListReq,
@@ -103,11 +104,11 @@ export class PluginController extends CrudController<PluginService> {
await this.auditLog({ content: "保存了公共插件配置" });
return this.ok(res);
}
@Post("/getPluginByName", { description: "sys:settings:view" })
async getPluginByName(@Body("name") name: string) {
@Post("/getSetting", { description: "sys:settings:view" })
async getSetting(@Body("name") name: string, @Body("type") type: string) {
const res = await this.pluginConfigService.getPluginConfig({
name: name,
type: "builtIn",
name,
type,
});
return this.ok(res);
}
@@ -192,6 +193,11 @@ export class PluginController extends CrudController<PluginService> {
return this.ok(res);
}
@Post("/online/author/update", { description: "sys:settings:edit", summary: "修改在线插件作者邮箱" })
async onlineAuthorUpdate(@Body(ALL) body: OnlinePluginAuthorUpdateReq) {
return this.ok(await this.service.updateOnlinePluginAuthor(body));
}
@Post("/online/publish/info", { description: "sys:settings:view", summary: "查询本地插件发布信息" })
async onlinePublishInfo(@Body(ALL) body: OnlinePluginPublishInfoReq) {
const res = await this.service.getOnlinePluginPublishInfo(body);
@@ -203,4 +209,10 @@ export class PluginController extends CrudController<PluginService> {
const res = await this.service.exportPlugin(id);
return this.ok(res);
}
@Post("/getPluginDefine", { description: "sys:settings:view", summary: "根据插件名称获取插件定义" })
async getPluginDefine(@Body("name") name: string) {
const res = await this.service.getPluginDefine(name);
return this.ok(res);
}
}
@@ -5,7 +5,12 @@ import { RoleService } from "../../../modules/sys/authority/service/role-service
import { PipelineService } from "../../../modules/pipeline/service/pipeline-service.js";
import { HistoryService } from "../../../modules/pipeline/service/history-service.js";
import { CertInfoService, CertStatus } from "../../../modules/monitor/index.js";
import { SiteInfoService } from "../../../modules/monitor/service/site-info-service.js";
import { DomainService } from "../../../modules/cert/service/domain-service.js";
import { Between, LessThan } from "typeorm";
import { ApiTags } from "@midwayjs/swagger";
import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js";
import { UserStatisticSetting } from "../../../modules/mine/service/models.js";
export type ChartItem = {
name: string;
@@ -25,7 +30,24 @@ export type UserStatisticCount = {
expiring: number;
notExpired: number;
};
siteCount?: {
total: number;
abnormal: number;
normal: number;
};
domainCount?: {
total: number;
expired: number;
expiring: number;
notExpired: number;
};
expiringList: any[];
genCertCount: {
singleDomainCertCount: number;
multiDomainCertCount: number;
wildcardCertCount: number;
totalPipelineRuns: number;
};
};
/**
*/
@@ -46,6 +68,15 @@ export class StatisticController extends BaseController {
@Inject()
certInfoService: CertInfoService;
@Inject()
siteInfoService: SiteInfoService;
@Inject()
domainService: DomainService;
@Inject()
userSettingsService: UserSettingsService;
@Post("/count", { description: Constants.per.authOnly, summary: "查询仪表盘统计数据" })
public async count() {
const { userId, projectId } = await this.getProjectUserIdRead();
@@ -57,14 +88,33 @@ export class StatisticController extends BaseController {
const expiringList = await this.pipelineService.latestExpiringList({ userId, projectId, count: 5 });
const certCount = await this.certInfoService.count({ userId, projectId, status: CertStatus.active });
const now = Date.now();
const oneMonthLater = now + 30 * 24 * 60 * 60 * 1000;
const userProjectQuery = this.siteInfoService.buildUserProjectQuery(userId, projectId);
const siteTotal = await this.siteInfoService.repository.count({ where: userProjectQuery });
const siteAbnormal = await this.siteInfoService.repository.count({
where: { ...userProjectQuery, checkStatus: "error" },
});
const domainProjectQuery = this.domainService.buildUserProjectQuery(userId, projectId);
const domainTotal = await this.domainService.repository.count({ where: domainProjectQuery });
const domainExpired = await this.domainService.repository.count({
where: { ...domainProjectQuery, expirationDate: LessThan(now) },
});
const domainExpiring = await this.domainService.repository.count({
where: { ...domainProjectQuery, expirationDate: Between(now, oneMonthLater) },
});
const userStatistic = await this.userSettingsService.getSetting<UserStatisticSetting>(userId, projectId, UserStatisticSetting);
const count: UserStatisticCount = {
pipelineCount,
pipelineStatusCount,
pipelineEnableCount,
certCount,
siteCount: { total: siteTotal, abnormal: siteAbnormal, normal: siteTotal - siteAbnormal },
domainCount: { total: domainTotal, expired: domainExpired, expiring: domainExpiring, notExpired: domainTotal - domainExpired - domainExpiring },
historyCountPerDay: historyCount,
expiringList,
genCertCount: userStatistic.genCertCount,
};
return this.ok(count);
}
@@ -201,12 +201,16 @@ export class PipelineController extends CrudController<PipelineService> {
@Post("/save", { description: Constants.per.authOnly, summary: "新增/更新流水线" })
async save(@Body() bean: PipelineSaveDTO) {
const { userId, projectId } = await this.getProjectUserIdWrite();
const isNew = bean.id <= 0;
if (bean.id < 0) {
throw new Error("流水线ID不能小于0");
}
const isNew = bean.id == 0 || bean.id == null;
if (!isNew) {
const { userId, projectId } = await this.checkOwner(this.getService(), bean.id, "write", true);
bean.userId = userId;
bean.projectId = projectId;
} else {
delete bean.id;
bean.userId = userId;
bean.projectId = projectId;
}
@@ -93,6 +93,7 @@ export class CommonEabToAcmeAccountFix {
...certApplyConfig.sysSetting,
input,
},
type: "builtIn",
});
}
return true;
@@ -108,11 +108,8 @@ describe("DnsPersistRecordService", () => {
param.id = 77;
saved = { ...param };
},
async findOneBy(where: any) {
return where.id === 77 ? saved : null;
},
async findOne() {
return null;
return saved;
},
};
(service as any).accessService = {
@@ -218,11 +215,11 @@ describe("DnsPersistRecordService", () => {
saved = { ...saved, ...param };
},
async findOne(options: any) {
if (options.where.id === 89) {
return saved;
}
return options.where.domain === "example.com" && options.where.acmeAccountAccessId === 1 ? saved : null;
},
async findOneBy(where: any) {
return where.id === 89 ? saved : null;
},
};
(service as any).accessService = {
async getAccessById() {
@@ -256,8 +253,8 @@ describe("DnsPersistRecordService", () => {
const service = new DnsPersistRecordService();
let deletedIds: any = null;
(service as any).repository = {
async findOneBy(where: any) {
return where.id === 90
async findOne(options: any) {
return options.where.id === 90
? {
id: 90,
domain: "example.com",
@@ -292,8 +289,8 @@ describe("DnsPersistRecordService", () => {
status: "pending",
};
(service as any).repository = {
async findOneBy(where: any) {
return where.id === 91 ? saved : null;
async findOne(options: any) {
return options.where.id === 91 ? saved : null;
},
async save(param: any) {
saved = { ...saved, ...param };
@@ -73,10 +73,11 @@ export class DomainService extends BaseService<DomainEntity> {
if (!param.domain) {
throw new Error("domain 不能为空");
}
const old = await this.repository.findOne({
const query = this.buildUserProjectQuery(param.userId, param.projectId);
const old = await this.findOne({
where: {
domain: param.domain,
userId: param.userId,
...query,
},
});
if (old) {
@@ -96,11 +97,11 @@ export class DomainService extends BaseService<DomainEntity> {
if (!old) {
throw new Error("domain记录不存在");
}
const query = this.buildUserProjectQuery(param.userId, param.projectId);
const same = await this.repository.findOne({
where: {
domain: param.domain,
userId: old.userId,
...query,
id: Not(param.id),
},
});
@@ -222,7 +223,6 @@ export class DomainService extends BaseService<DomainEntity> {
}
domainVerifiers[domain] = null;
}
return domainVerifiers;
}
@@ -52,6 +52,20 @@ export class UserGrantSetting extends BaseSettings {
allowAdminViewCerts = false;
}
export class UserStatisticSetting extends BaseSettings {
static __title__ = "用户累计统计";
static __key__ = "user.statistic";
genCertCount = {
singleDomainCertCount: 0,
multiDomainCertCount: 0,
wildcardCertCount: 0,
totalPipelineRuns: 0,
};
}
export type UserStatisticField = `genCertCount.${keyof UserStatisticSetting["genCertCount"]}`;
export class UserPreferencesSetting extends BaseSettings {
static __title__ = "用户偏好设置";
static __key__ = "user.preferences";
@@ -3,7 +3,9 @@ import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { BaseService, BaseSettings } from "@certd/lib-server";
import { UserSettingsEntity } from "../entity/user-settings.js";
import { LocalCache, mergeUtils } from "@certd/basic";
import { LocalCache, locker, mergeUtils } from "@certd/basic";
import { UserStatisticField, UserStatisticSetting } from "./models.js";
import { get, set } from "lodash-es";
const { merge } = mergeUtils;
const UserSettingCache = new LocalCache({
@@ -133,4 +135,14 @@ export class UserSettingsService extends BaseService<UserSettingsEntity> {
newEntity.setting = JSON.stringify(bean);
await this.repository.save(newEntity);
}
async incrementStatistic(userId: number, projectId: number | null | undefined, field: UserStatisticField) {
const lockKey = `user-statistic:${userId}:${projectId ?? "global"}`;
await locker.execute(lockKey, async () => {
const setting = await this.getSetting<UserStatisticSetting>(userId, projectId ?? undefined, UserStatisticSetting);
const current = Number(get(setting, field, 0)) || 0;
set(setting, field, current + 1);
await this.saveSetting(userId, projectId ?? undefined, setting);
});
}
}
@@ -23,6 +23,14 @@ function createSelfSignedCert(domain = "example.com") {
};
}
function createCertInfoService() {
const service = new CertInfoService();
service.userSettingsService = {
async incrementStatistic() {},
} as any;
return service;
}
describe("CertInfoService", () => {
it("counts wildcard domains by normalized prefix", () => {
const service = new CertInfoService();
@@ -32,7 +40,7 @@ describe("CertInfoService", () => {
describe("updateCertByPipelineId 多条模式", () => {
it("新建激活证书并绑定流水线id、继承已有记录来源", async () => {
const service = new CertInfoService();
const service = createCertInfoService();
// 该流水线已有旧证书记录(upload 来源),但没有空证书记录
const existing = { id: 10, userId: 2, projectId: 3, pipelineId: 1, fromType: "upload", status: CertStatus.inactive };
service.repository = {
@@ -74,8 +82,11 @@ describe("CertInfoService", () => {
});
it("无已有记录时按流水线类型推导来源,并把同流水线旧激活证书标记为未激活", async () => {
const service = new CertInfoService();
const service = createCertInfoService();
service.repository = {
async find() {
return [];
},
async findOne() {
return null;
},
@@ -117,8 +128,11 @@ describe("CertInfoService", () => {
});
it("无已有记录且流水线类型无对应来源时,使用传入来源", async () => {
const service = new CertInfoService();
const service = createCertInfoService();
service.repository = {
async find() {
return [];
},
async findOne() {
return null;
},
@@ -143,8 +157,11 @@ describe("CertInfoService", () => {
});
it("传申请任务id时,新证书记录任务id,且只把同任务旧证书标记为未激活", async () => {
const service = new CertInfoService();
const service = createCertInfoService();
service.repository = {
async find() {
return [];
},
async findOne() {
return null;
},
@@ -182,10 +199,13 @@ describe("CertInfoService", () => {
});
it("存在空证书记录时,申请成功后直接更新占位记录,不新建记录", async () => {
const service = new CertInfoService();
const service = createCertInfoService();
// 保存流水线时创建的空证书记录(占位,certInfo 为空,已记录申请任务id)
const emptyRecord = { id: 184, userId: 2, projectId: 1, pipelineId: 209, fromType: "pipeline", status: CertStatus.active, taskId: "PgaUhdeO1DWB_BLbIlzqV", certInfo: null };
service.repository = {
async find() {
return [];
},
async findOne() {
// 既有记录查询与空证书记录查询都命中同一条占位记录
return emptyRecord;
@@ -228,7 +248,7 @@ describe("CertInfoService", () => {
});
it("updateCertByPipelineId 从流水线配置解析并记录 ACME账号授权id(供吊销读表,不再查流水线)", async () => {
const service = new CertInfoService();
const service = createCertInfoService();
service.repository = {
async findOne(args: any) {
const where = args?.where ?? args;
@@ -282,6 +302,9 @@ describe("CertInfoService", () => {
it("updateDomains 保存流水线时为申请任务创建空证书记录(记录任务id)", async () => {
const service = new CertInfoService();
service.repository = {
async find() {
return [];
},
async findOne() {
return null;
},
@@ -312,6 +335,9 @@ describe("CertInfoService", () => {
const service = new CertInfoService();
const existing = { id: 10, pipelineId: 1, userId: 2, projectId: 3, fromType: "auto", taskId: "apply-step-1", status: CertStatus.active };
service.repository = {
async find() {
return [existing];
},
async findOne() {
return existing;
},
@@ -337,6 +363,9 @@ describe("CertInfoService", () => {
const service = new CertInfoService();
let findOneCount = 0;
service.repository = {
async find() {
return [];
},
async findOne() {
findOneCount++;
return null;
@@ -374,6 +403,9 @@ describe("CertInfoService", () => {
it("updateDomains 删除孤儿 active 记录(流水线中已不存在的任务,含 taskId 为空的遗留记录)", async () => {
const service = new CertInfoService();
service.repository = {
async find() {
return [];
},
async findOne() {
return null;
},
@@ -7,6 +7,7 @@ import { logger, utils } from "@certd/basic";
import { CertInfo, CertReader } from "@certd/plugin-cert";
import { AcmeService } from "../../../plugins/plugin-cert/plugin/cert-plugin/acme.js";
import { PipelineEntity } from "../../pipeline/entity/pipeline.js";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
export type UploadCertReq = {
id?: number;
@@ -39,6 +40,9 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
@InjectEntityModel(PipelineEntity)
pipelineRepository: Repository<PipelineEntity>;
@Inject()
userSettingsService: UserSettingsService;
//@ts-ignore
getRepository() {
return this.repository;
@@ -213,6 +217,12 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
status: CertStatus.inactive,
}
);
if (certUserId != null) {
const domains = cert?.crt ? new CertReader(cert).getAltNames() : [];
const wildcardCount = domains.filter(domain => String(domain).trim().toLowerCase().startsWith("*.")).length;
const field = wildcardCount > 0 ? "genCertCount.wildcardCertCount" : domains.length > 1 ? "genCertCount.multiDomainCertCount" : "genCertCount.singleDomainCertCount";
await this.userSettingsService.incrementStatistic(certUserId, certProjectId, field);
}
return bean;
}
@@ -243,20 +253,28 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
}
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
const taskIds = applyTasks.map(task => task.taskId);
const foundHistory = await this.repository.find({
where: {
pipelineId,
status: CertStatus.active,
...userProjectQuery,
},
});
for (const task of applyTasks) {
// 每个申请任务维护一条 active 记录(不重复创建,最新真证书申请成功后旧记录会被标记为 inactive)
const found = await this.repository.findOne({
where: {
pipelineId,
taskId: task.taskId,
status: CertStatus.active,
...userProjectQuery,
},
});
const bean = new CertInfoEntity();
let found = foundHistory.find(item => item.taskId === task.taskId);
if (!found && foundHistory.length == 1 && applyTasks.length == 1) {
found = foundHistory[0];
}
if (found) {
//已有 active 记录(空占位或真证书):更新域名信息
bean.id = found.id;
if (!found.taskId) {
bean.taskId = task.taskId;
}
} else {
//创建空证书记录
bean.pipelineId = pipelineId;
@@ -281,7 +299,6 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
taskId: Or(IsNull(), Not(In(taskIds))),
});
}
/**
* 根据流水线类型推导证书来源(与流水线保存逻辑保持一致)
*/
@@ -33,6 +33,7 @@ import { ProjectService } from "../../sys/enterprise/service/project-service.js"
import { CertApplyStepInputPatch, updateCertApplyStepInputs } from "./pipeline-batch-update.js";
import { calcNextSuiteCountUsed } from "./pipeline-suite-limit.js";
import { CertApplyTemplateParams } from "../../cert/service/cert-apply-template-fields.js";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
const runningTasks: Map<string | number, Executor> = new Map();
/**
@@ -90,6 +91,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
@Inject()
projectService: ProjectService;
@Inject()
userSettingsService: UserSettingsService;
//@ts-ignore
getRepository() {
@@ -786,6 +789,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
};
const historyId = await this.historyService.start(entity, triggerType);
await this.userSettingsService.incrementStatistic(entity.userId, entity.projectId, "genCertCount.totalPipelineRuns");
const sysInfo: SysInfo = {};
if (isComm()) {
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
@@ -915,6 +919,15 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
return pipelineEntity.projectId;
}
async getUserProjectId(pipelineId: number) {
const pipelineEntity = await this.repository.findOne({
select: { userId: true, projectId: true },
where: { id: pipelineId },
});
return pipelineEntity ? { userId: pipelineEntity.userId, projectId: pipelineEntity.projectId } : null;
}
private async saveHistory(history: RunHistory) {
//修改pipeline状态
const pipelineEntity = new PipelineEntity();
@@ -8,9 +8,9 @@ export class PluginConfigGetter implements IPluginConfigService {
@Inject()
pluginConfigService: PluginConfigService;
async getPluginConfig(pluginName: string): Promise<PluginConfig> {
async getPluginConfig(name: string): Promise<PluginConfig> {
const res = await this.pluginConfigService.getPluginConfig({
name: pluginName,
name,
type: "builtIn",
});
return {
@@ -4,7 +4,8 @@ import { PluginService } from "./plugin-service.js";
export type PluginConfig = {
name: string;
disabled?: boolean;
sysSetting: {
type: string;
sysSetting?: {
input?: Record<string, any>;
metadata?: Record<string, any>;
};
@@ -38,6 +39,7 @@ export class PluginConfigService {
async saveCommPluginConfig(config: CommPluginConfig) {
config.CertApply.name = "CertApply";
config.CertApply.type = "builtIn";
await this.savePluginConfig(config.CertApply);
}
@@ -47,17 +49,23 @@ export class PluginConfigService {
if (!sysSetting) {
throw new Error(`${name}.sysSetting is required`);
}
const pluginEntity = await this.pluginService.getRepository().findOne({
where: { name },
let pluginEntity: any = await this.pluginService.getRepository().findOne({
where: { fullName: name, type: config.type },
});
if (!pluginEntity) {
await this.pluginService.add({
name,
if (config.type !== "builtIn") {
//只有内置插件才需要add config
throw new Error(`${name}.type must be builtIn`);
}
pluginEntity = {
name: name,
fullName: name,
sysSetting: JSON.stringify(sysSetting),
type: "builtIn",
disabled: false,
author: "certd",
});
};
const { id } = await this.pluginService.add(pluginEntity);
pluginEntity.id = id;
} else {
const setting = JSON.parse(pluginEntity.sysSetting || "{}");
if (sysSetting.metadata) {
@@ -66,18 +74,18 @@ export class PluginConfigService {
if (sysSetting.input) {
setting.input = sysSetting.input;
}
await this.pluginService.getRepository().update({ name }, { sysSetting: JSON.stringify(setting) });
await this.pluginService.getRepository().update({ fullName: name }, { sysSetting: JSON.stringify(setting) });
}
}
async get(req: PluginFindReq) {
if (!req.name && !req.id) {
throw new Error("plugin s name or id is required");
throw new Error("plugin name or id is required");
}
return await this.pluginService.getRepository().findOne({
where: {
id: req.id,
name: req.name,
fullName: req.name,
type: req.type,
},
});
@@ -98,8 +106,9 @@ export class PluginConfigService {
sysSetting = JSON.parse(plugin.sysSetting);
}
return {
name: plugin.name,
name: plugin.fullName,
disabled: plugin.disabled,
type: plugin.type,
sysSetting,
};
}
@@ -13,6 +13,7 @@ type RepoOptions = {
function createPluginRepositoryMock(options: RepoOptions = {}) {
const state = {
savedRows: [] as any[],
saveCalls: [] as any[][],
updates: [] as any[],
findCalls: 0,
queryRows: options.queryRows || options.marketRows || [],
@@ -198,6 +199,7 @@ function createPluginRepositoryMock(options: RepoOptions = {}) {
},
async save(records: any[] | any) {
const list = Array.isArray(records) ? records : [records];
state.saveCalls.push(list);
state.savedRows.push(...list);
return records;
},
@@ -262,11 +264,14 @@ describe("PluginService online plugins", () => {
);
});
it("requires a user-provided author for local store plugin edits", async () => {
it("allows empty author for local store plugin edits", () => {
const service = new PluginService();
await assert.rejects(() => service.add({ type: "store", author: "local", name: "demo", pluginType: "deploy" }), /请先填写插件作者/);
await assert.rejects(() => service.update({ id: 1, type: "store", author: "", name: "demo" }), /请先填写插件作者/);
const plugin = { author: "", name: "demo" };
(service as any).normalizeStorePluginAuthor(plugin);
assert.equal(plugin.author, "");
assert.equal((plugin as any).fullName, undefined);
});
it("creates a notification plugin with the default template", async () => {
@@ -459,19 +464,13 @@ describe("PluginService online plugins", () => {
const service = new PluginService();
const savedSettings: any[] = [];
const requestCalls: any[] = [];
let createdAtLt: number | undefined;
service.plusService = {
async register() {},
async request(config: any) {
requestCalls.push(config);
assert.equal(config.url, "/activation/plugin/page");
assert.equal(config.data.page.limit, 200);
assert.equal(typeof config.data.createdAtLt, "number");
if (createdAtLt === undefined) {
createdAtLt = config.data.createdAtLt;
} else {
assert.equal(config.data.createdAtLt, createdAtLt);
}
assert.equal(config.data.createdAtLt, undefined);
if (config.data.page.start === 0) {
return {
page: {
@@ -538,6 +537,11 @@ describe("PluginService online plugins", () => {
const res = await service.syncOnlinePluginList();
assert.equal(requestCalls.length, 2);
assert.equal(repository.state.saveCalls.length, 201);
assert.equal(
repository.state.saveCalls.every(records => records.length === 1),
true
);
assert.equal(res.length, 201);
assert.equal(repository.state.savedRows.length, 201);
assert.equal(repository.state.savedRows[0].fullName, "developer/plugin-1");
@@ -695,10 +699,7 @@ describe("PluginService online plugins", () => {
assert.deepEqual(
dependencies.map(item => item.fullName),
[
"developer/access-b",
"developer/access-a",
]
["developer/access-b", "developer/access-a"]
);
assert.deepEqual(findCalls, [
{ type: "store", fullName: "developer/target" },
@@ -896,10 +897,7 @@ describe("PluginService online plugins", () => {
assert.match(importReq.content, /DeployToDemo/);
assert.match(importReq.content, /^author: greper$/m);
assert.match(importReq.content, /^fullName: greper\/DeployToDemo$/m);
assert.deepEqual(downloadUpdates[0].where, {
type: "store",
fullName: "greper/DeployToDemo",
});
assert.deepEqual(downloadUpdates, []);
});
it("preserves dependPlugins when syncing online plugin metadata", async () => {
@@ -940,7 +938,7 @@ describe("PluginService online plugins", () => {
// 远程下掉依赖后,专用列不再保留旧声明。
assert.equal(record.dependPlugins, null);
assert.deepEqual(yaml.load(record.extra), { dependPackages: { axios: "*" } });
assert.equal(record.extra, null);
});
it("reads dependPlugins from the dedicated column", async () => {
@@ -1009,7 +1007,8 @@ describe("PluginService online plugins", () => {
} as any;
service.add = async (param: any) => {
param.id = 1;
return { id: 1 };
savedPlugin = param;
return param;
};
service.unRegisterById = async () => {};
service.registerById = async () => {};
@@ -1062,7 +1061,7 @@ describe("PluginService online plugins", () => {
it("preserves synced market ownership when installing plugin content", async () => {
const service = new PluginService();
let updatedPlugin: any;
let savedPlugin: any;
service.repository = {
async findOne() {
@@ -1079,10 +1078,11 @@ describe("PluginService online plugins", () => {
syncTime: 123,
};
},
async save(plugin: any) {
savedPlugin = plugin;
return plugin;
},
} as any;
service.update = async plugin => {
updatedPlugin = plugin;
};
await service.importPlugin({
type: "store",
@@ -1090,37 +1090,37 @@ describe("PluginService online plugins", () => {
content: "name: online-plugin\npluginType: deploy\nauthor: greper\ncontent: |\n return class Demo {}\n",
});
assert.equal(updatedPlugin.id, 7);
assert.equal(updatedPlugin.appId, 3);
assert.equal(updatedPlugin.developerId, 12);
assert.equal(updatedPlugin.latest, "1.0.1");
assert.equal(updatedPlugin.status, "published");
assert.equal(updatedPlugin.downloadCount, 20);
assert.equal(updatedPlugin.score, 4.5);
assert.equal(updatedPlugin.syncTime, 123);
assert.equal(updatedPlugin.installed, true);
assert.equal(savedPlugin.id, 7);
assert.equal(savedPlugin.appId, 3);
assert.equal(savedPlugin.developerId, 12);
assert.equal(savedPlugin.latest, "1.0.1");
assert.equal(savedPlugin.status, "published");
assert.equal(savedPlugin.downloadCount, 20);
assert.equal(savedPlugin.score, 4.5);
assert.equal(typeof savedPlugin.syncTime, "number");
assert.equal(savedPlugin.syncTime >= 123, true);
assert.equal(savedPlugin.installed, true);
});
it("reuses a legacy custom plugin when switching an online plugin version", async () => {
it("imports an online plugin as a new store record when only a legacy custom plugin exists", async () => {
const service = new PluginService();
const findOptions: any[] = [];
let updatedPlugin: any;
let savedPlugin: any;
(service as any).findOne = async (options: any) => {
findOptions.push(options);
if (findOptions.length === 1) {
return null;
};
service.repository = {
async findOne() {
return null;
}
return {
id: 7,
type: "custom",
author: "greper2",
name: "BaishanUpdateCert666",
};
};
service.update = async plugin => {
updatedPlugin = plugin;
};
},
async save(plugin: any) {
plugin.id = 8;
savedPlugin = plugin;
return plugin;
},
} as any;
await service.importPlugin({
type: "store",
@@ -1129,13 +1129,14 @@ describe("PluginService online plugins", () => {
});
assert.equal(findOptions.length, 2);
assert.deepEqual(findOptions[1].where, {
author: "greper2",
name: "BaishanUpdateCert666",
});
assert.equal(updatedPlugin.id, 7);
assert.equal(updatedPlugin.type, "store");
assert.equal(updatedPlugin.installed, true);
for (const options of findOptions) {
assert.deepEqual(options.where, {
type: "store",
fullName: "greper2/BaishanUpdateCert666",
});
}
assert.equal(savedPlugin.type, "store");
assert.equal(savedPlugin.installed, true);
});
it("updates a store plugin without conflicting with a legacy custom plugin of the same name", async () => {
@@ -1174,7 +1175,7 @@ describe("PluginService online plugins", () => {
it("marks an uninstalled market plugin as installed after importing it again", async () => {
const service = new PluginService();
let updatedPlugin: any;
let savedPlugin: any;
service.repository = {
async findOne() {
@@ -1187,10 +1188,11 @@ describe("PluginService online plugins", () => {
content: "",
};
},
async save(plugin: any) {
savedPlugin = plugin;
return plugin;
},
} as any;
service.update = async plugin => {
updatedPlugin = plugin;
};
await service.importPlugin({
type: "store",
@@ -1198,7 +1200,7 @@ describe("PluginService online plugins", () => {
content: "name: online-plugin\npluginType: deploy\nauthor: greper\ncontent: |\n return class Demo {}\n",
});
assert.equal(updatedPlugin.installed, true);
assert.equal(savedPlugin.installed, true);
});
it("publishes a local store plugin through activation with exported content", async () => {
@@ -1266,10 +1268,7 @@ describe("PluginService online plugins", () => {
assert.equal(requestConfig.data.version, "1.2.3");
assert.equal(requestConfig.data.minAppVersion, "");
assert.equal(requestConfig.data.maxAppVersion, "");
assert.equal(updatedPlugin.appId, 3);
assert.equal(updatedPlugin.developerId, 12);
assert.equal(updatedPlugin.author, "developer");
assert.equal(updatedPlugin.fullName, "developer/custom-one");
assert.equal(updatedPlugin, undefined);
});
it("gets and creates online plugin author for the bound user", async () => {
@@ -1300,7 +1299,7 @@ describe("PluginService online plugins", () => {
const authorGet = await service.getOnlinePluginAuthor();
const authorAdd = await service.addOnlinePluginAuthor({
name: "developer",
displayName: "Developer",
email: "developer@example.com",
});
assert.equal(authorGet.registered, false);
@@ -1315,9 +1314,7 @@ describe("PluginService online plugins", () => {
method: "post",
data: {
name: "developer",
displayName: "Developer",
avatar: "",
desc: "",
email: "developer@example.com",
},
});
});
@@ -1482,7 +1479,7 @@ describe("PluginService online plugins", () => {
assert.deepEqual(deleted, []);
});
it("persists dependPlugins from the edit form (object form) through update", async () => {
it("passes dependPlugins object from the edit form through update", async () => {
const service = new PluginService();
let savedPlugin: any;
@@ -1515,8 +1512,8 @@ describe("PluginService online plugins", () => {
},
});
assert.deepEqual(yaml.load(savedPlugin.dependPlugins), { "access:aliyun": "*" });
assert.deepEqual(yaml.load(savedPlugin.extra), {
assert.deepEqual(savedPlugin.dependPlugins, { "access:aliyun": "*" });
assert.deepEqual(savedPlugin.extra, {
showRunStrategy: false,
dependPackages: ["example-sdk"],
});
@@ -1552,7 +1549,7 @@ describe("PluginService online plugins", () => {
assert.deepEqual(yaml.load(savedPlugin.dependPlugins), { "access:aliyun": "*" });
});
it("only updates editable fields and preserves market synchronization fields", async () => {
it("passes submitted market synchronization fields through update", async () => {
const service = new PluginService();
let savedPlugin: any;
@@ -1607,16 +1604,16 @@ describe("PluginService online plugins", () => {
assert.equal(savedPlugin.title, "更新后的标题");
assert.equal(savedPlugin.version, "1.3.0");
assert.equal(savedPlugin.appId, undefined);
assert.equal(savedPlugin.developerId, undefined);
assert.equal(savedPlugin.latest, undefined);
assert.equal(savedPlugin.status, undefined);
assert.equal(savedPlugin.downloadCount, undefined);
assert.equal(savedPlugin.score, undefined);
assert.equal(savedPlugin.aiCheckStatus, undefined);
assert.equal(savedPlugin.syncTime, undefined);
assert.equal(savedPlugin.installed, undefined);
assert.equal(savedPlugin.editable, undefined);
assert.equal(savedPlugin.appId, 999);
assert.equal(savedPlugin.developerId, 999);
assert.equal(savedPlugin.latest, "9.9.9");
assert.equal(savedPlugin.status, "offline");
assert.equal(savedPlugin.downloadCount, 0);
assert.equal(savedPlugin.score, 0);
assert.equal(savedPlugin.aiCheckStatus, "rejected");
assert.equal(savedPlugin.syncTime, 999);
assert.equal(savedPlugin.installed, false);
assert.equal(savedPlugin.editable, false);
});
it("exports dependPlugins back into the plugin YAML", async () => {
@@ -2,7 +2,7 @@ import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { addonRegistry, BaseService, PageReq, PlusService, SysInstallInfo, SysPluginSetting, SysSettingsService } from "@certd/lib-server";
import { PluginEntity } from "../entity/plugin.js";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Brackets, Equal, IsNull, Like, Not, Or, Repository } from "typeorm";
import { Brackets, Equal, In, IsNull, Like, Not, Or, Repository } from "typeorm";
import { isComm } from "@certd/plus-core";
import { BuiltInPluginService } from "../../pipeline/service/builtin-plugin-service.js";
import { merge } from "lodash-es";
@@ -85,6 +85,7 @@ export type OnlinePluginAuthorBean = {
avatar?: string;
desc?: string;
status?: string;
email?: string;
};
export type OnlinePluginAuthorGetReply = {
@@ -93,11 +94,13 @@ export type OnlinePluginAuthorGetReply = {
};
export type OnlinePluginAuthorAddReq = {
name: string;
name?: string;
displayName?: string;
avatar?: string;
desc?: string;
email?: string;
};
export type OnlinePluginAuthorUpdateReq = { email: string };
export type OnlinePluginBean = {
id?: number;
@@ -535,7 +538,7 @@ export class PluginService extends BaseService<PluginEntity> {
const settingPlugins = await this.find({
select: {
id: true,
name: true,
fullName: true,
sysSetting: true,
},
where: {
@@ -548,7 +551,7 @@ export class PluginService extends BaseService<PluginEntity> {
if (!item.sysSetting) {
continue;
}
pluginSettingMap[item.name] = JSON.parse(item.sysSetting);
pluginSettingMap[item.fullName] = JSON.parse(item.sysSetting);
}
for (const key in groups) {
const group = groups[key];
@@ -556,7 +559,7 @@ export class PluginService extends BaseService<PluginEntity> {
continue;
}
for (const item of group.plugins) {
const pluginSetting = pluginSettingMap[item.name];
const pluginSetting = pluginSettingMap[item.fullName];
if (pluginSetting) {
item.sysSetting = pluginSetting;
}
@@ -569,7 +572,7 @@ export class PluginService extends BaseService<PluginEntity> {
disabled: true,
},
});
const disabledNames = list.map(it => it.name);
const disabledNames = list.map(it => it.fullName);
for (const key in groups) {
const group = groups[key];
if (!group.plugins) {
@@ -659,31 +662,28 @@ export class PluginService extends BaseService<PluginEntity> {
/**
* 表单可能直接提交对象形式的 dependPlugins(KvInput),落库前统一序列化为 YAML 字符串。
*/
private normalizeDependPluginsParam(param: any) {
if (param.dependPlugins && typeof param.dependPlugins === "object" && !Array.isArray(param.dependPlugins)) {
param.dependPlugins = toDependPluginsYaml(param.dependPlugins);
}
}
// private normalizeDependPluginsParam(param: any) {
// if (param.dependPlugins && typeof param.dependPlugins === "object" && !Array.isArray(param.dependPlugins)) {
// param.dependPlugins = toDependPluginsYaml(param.dependPlugins);
// }
// }
private normalizePluginYamlParam(param: any, field: "extra" | "dependPlugins") {
const value = param[field];
if (value && typeof value === "object" && !Array.isArray(value)) {
param[field] = yaml.dump(value, {
lineWidth: -1,
noRefs: true,
sortKeys: false,
});
}
}
// private normalizePluginYamlParam(param: any, field: "extra" | "dependPlugins") {
// const value = param[field];
// if (value && typeof value === "object" && !Array.isArray(value)) {
// param[field] = yaml.dump(value, {
// lineWidth: -1,
// noRefs: true,
// sortKeys: false,
// });
// }
// }
private normalizeStorePluginAuthor(plugin: Record<string, any>) {
const author = `${plugin.author || ""}`.trim();
if (!author || author.toLowerCase() === "local") {
throw new Error("请先填写插件作者");
}
plugin.author = author;
const name = `${plugin.name || ""}`.trim();
if (name) {
if (name && author) {
plugin.fullName = `${author}/${name}`;
}
}
@@ -693,24 +693,26 @@ export class PluginService extends BaseService<PluginEntity> {
* @param param 数据
*/
async add(param: any) {
this.normalizeDependPluginsParam(param);
// this.normalizeDependPluginsParam(param);
param.type = normalizePluginSourceType(param.type);
if (param.type === "store") {
this.normalizeStorePluginAuthor(param);
}
const old = await this.findOne({
where: {
type: "store",
type: param.type,
fullName: param.fullName,
},
});
if (old) {
throw new Error(`插件${param.author}/${param.name}已存在`);
throw new Error(`插件${param.fullName}已存在`);
}
if (param.type === "builtIn") {
return await super.add({
disabled: false,
installed: true,
...param,
});
}
@@ -732,8 +734,10 @@ export class PluginService extends BaseService<PluginEntity> {
}
const res = await super.add({
disabled: false,
...param,
...plugin,
installed: true,
});
await this.registerById(res.id);
@@ -748,6 +752,9 @@ export class PluginService extends BaseService<PluginEntity> {
if (item.type === "builtIn") {
return;
}
if (!item.installed) {
return;
}
await this.registerPlugin(item);
await this.refreshPluginDeps();
}
@@ -762,19 +769,10 @@ export class PluginService extends BaseService<PluginEntity> {
return "";
}
private normalizeOnlinePluginRecord(item: OnlinePluginBean, syncTime: number, old?: PluginEntity) {
private normalizeOnlinePluginRecord(item: OnlinePluginBean, syncTime: number) {
const fullName = this.getOnlinePluginFullName(item);
let extra: Record<string, any> = {};
if (old?.extra) {
try {
extra = (yaml.load(old.extra) as Record<string, any>) || {};
} catch (e) {
extra = {};
}
}
delete extra.dependPlugins;
return this.repository.create({
id: old?.id,
const extra: Record<string, any> = {};
return {
appId: item.appId,
developerId: item.developerId,
fullName,
@@ -794,16 +792,9 @@ export class PluginService extends BaseService<PluginEntity> {
dependPlugins: toDependPluginsYaml(item.dependPlugins),
extra: Object.keys(extra).length > 0 ? yaml.dump(extra) : null,
syncTime,
type: old?.type || "store",
disabled: old?.disabled ?? false,
installed: old?.installed ?? false,
setting: old?.setting,
sysSetting: old?.sysSetting,
content: old?.content,
metadata: old?.metadata,
version: old?.version,
type: "store",
updateTime: new Date(),
});
};
}
private toOnlinePluginBean(item: PluginEntity): OnlinePluginBean {
@@ -971,15 +962,13 @@ export class PluginService extends BaseService<PluginEntity> {
await this.plusService.register();
// 只同步市场列表元数据,插件 YAML 内容仍然在安装时按版本下载。
const pluginMap = new Map<string, OnlinePluginBean>();
const createdAtLt = Date.now();
const syncTime = new Date().getTime();
let pageStart = 0;
while (true) {
const res = await this.plusService.request({
url: "/activation/plugin/page",
method: "post",
data: {
createdAtLt,
page: {
start: pageStart,
limit: ONLINE_PLUGIN_SYNC_PAGE_SIZE,
@@ -993,14 +982,45 @@ export class PluginService extends BaseService<PluginEntity> {
},
});
const list = res?.list || [];
const fullNames = list.map(item => this.getOnlinePluginFullName(item));
const existingRecords = await this.repository.find({
where: {
type: "store",
fullName: In(fullNames),
},
});
const existingMap = new Map(existingRecords.map(item => [item.fullName, item]));
for (const item of list) {
const record = { ...item };
const fullName = this.getOnlinePluginFullName(record);
if (!fullName) {
const itemFromStore: any = this.normalizeOnlinePluginRecord(item, syncTime);
const fullName = itemFromStore.fullName;
const existingRecord = existingMap.get(fullName);
if (existingRecord) {
//本地已存在,只更新市场相关字段
if (existingRecord.installed) {
//如果本地已经安装的,则只同步版本 分数 下载量
const updateBean: Record<string, any> = {
id: existingRecord.id,
score: itemFromStore.score,
downloadCount: itemFromStore.downloadCount,
latestVersion: itemFromStore.version,
};
await this.updateById(updateBean);
} else {
//如果本地未安装的,则同步所有字段
const updateBean: Record<string, any> = {
id: existingRecord.id,
...itemFromStore,
installed: false,
};
await this.updateById(updateBean);
}
continue;
} else {
itemFromStore.disabled = false;
itemFromStore.installed = false;
await this.repository.save(itemFromStore);
}
record.fullName = fullName;
pluginMap.set(fullName, record);
}
const total = Number(res?.page?.total || 0);
if (total > 0) {
@@ -1015,23 +1035,6 @@ export class PluginService extends BaseService<PluginEntity> {
}
pageStart += ONLINE_PLUGIN_SYNC_PAGE_SIZE;
}
const existingList = await this.find({
where: {
type: "store",
},
});
const existingMap = new Map<string, PluginEntity>();
for (const item of existingList) {
const fullName = item.fullName || this.getOnlinePluginFullName(item as any);
if (fullName) {
existingMap.set(fullName, item);
}
}
const syncTime = Date.now();
const records = Array.from(pluginMap.values()).map(item => {
return this.normalizeOnlinePluginRecord(item, syncTime, existingMap.get(item.fullName));
});
await this.addOrUpdate(records);
await this.saveOnlinePluginSyncTime(syncTime);
return await this.onlinePluginList({});
}
@@ -1074,7 +1077,7 @@ export class PluginService extends BaseService<PluginEntity> {
override: true,
type: "store",
});
await this.refreshOnlinePluginDownloadCount(res.fullName || fullName, res.plugin?.downloadCount);
// await this.refreshOnlinePluginDownloadCount(res.fullName || fullName, res.plugin?.downloadCount);
return {
...importRes,
fullName: res.fullName || fullName,
@@ -1165,20 +1168,6 @@ export class PluginService extends BaseService<PluginEntity> {
maxAppVersion: req.maxAppVersion || "",
},
});
const marketPlugin = publishReply?.plugin as OnlinePluginBean | undefined;
const publishedAuthor = marketPlugin?.author || author.name;
const publishedFullName = marketPlugin?.fullName || `${publishedAuthor}/${plugin.name}`;
await this.update({
...plugin,
appId: marketPlugin?.appId || plugin.appId,
developerId: marketPlugin?.developerId || installInfo.bindUserId,
author: publishedAuthor,
fullName: publishedFullName,
latest: marketPlugin?.latest || plugin.latest,
status: marketPlugin?.status || plugin.status,
vip: marketPlugin?.vip || plugin.vip,
});
return publishReply;
}
@@ -1219,6 +1208,9 @@ export class PluginService extends BaseService<PluginEntity> {
}
async addOnlinePluginAuthor(req: OnlinePluginAuthorAddReq): Promise<OnlinePluginAuthorBean> {
if (!/^\S+@\S+\.\S+$/.test(req.email?.trim() || "")) {
throw new Error("请输入有效的作者邮箱");
}
await this.getBindUserId("注册插件作者");
await this.plusService.register();
return await this.plusService.request({
@@ -1226,13 +1218,20 @@ export class PluginService extends BaseService<PluginEntity> {
method: "post",
data: {
name: req.name,
displayName: req.displayName || "",
avatar: req.avatar || "",
desc: req.desc || "",
email: req.email,
},
});
}
async updateOnlinePluginAuthor(req: OnlinePluginAuthorUpdateReq): Promise<OnlinePluginAuthorBean> {
if (!/^\S+@\S+\.\S+$/.test(req.email?.trim() || "")) {
throw new Error("请输入有效的作者邮箱");
}
await this.getBindUserId("修改插件作者");
await this.plusService.register();
return await this.plusService.request({ url: "/activation/plugin/author/update", method: "post", data: { email: req.email } });
}
async getOnlinePluginPublishInfo(req: OnlinePluginPublishInfoReq): Promise<OnlinePluginPublishInfo> {
if (!req.id) {
throw new Error("插件ID不能为空");
@@ -1289,20 +1288,20 @@ export class PluginService extends BaseService<PluginEntity> {
await this.sysSettingsService.saveSetting(setting);
}
private async refreshOnlinePluginDownloadCount(fullName: string, downloadCount?: number) {
if (!fullName || downloadCount == null) {
return;
}
await this.updateWhere(
{
type: "store",
fullName,
},
{
downloadCount,
}
);
}
// private async refreshOnlinePluginDownloadCount(fullName: string, downloadCount?: number) {
// if (!fullName || downloadCount == null) {
// return;
// }
// await this.updateWhere(
// {
// type: "store",
// fullName,
// },
// {
// downloadCount,
// }
// );
// }
async unRegisterById(id: any) {
const item = await this.info(id);
@@ -1312,6 +1311,9 @@ export class PluginService extends BaseService<PluginEntity> {
if (item.type === "builtIn") {
return;
}
if (!item.installed) {
return;
}
const metadata = item.metadata ? yaml.load(item.metadata) : {};
const extra = item.extra ? yaml.load(item.extra) : {};
const plugin = {
@@ -1345,55 +1347,13 @@ export class PluginService extends BaseService<PluginEntity> {
if (!param?.id) {
throw new Error("id 不能为空");
}
// 编辑接口只允许修改插件自身内容,市场同步字段始终以数据库记录为准。
const current = this.repository?.findOneBy ? await this.info(param.id) : null;
const source = { ...(current || {}), ...param };
source.type = normalizePluginSourceType(current?.type || param.type);
if (source.type === "store") {
this.normalizeStorePluginAuthor(source);
}
let old: PluginEntity | null;
if (source.type === "store" && source.fullName) {
old = await this.findOne({
where: {
type: "store",
fullName: source.fullName,
},
});
} else {
old = await this.findOne({
where: {
name: source.name,
author: source.author,
},
});
}
if (old && old.id !== param.id) {
throw new Error(`插件${source.author}/${source.name}已存在`);
}
const editableFields = ["name", "author", "fullName", "icon", "title", "group", "desc", "setting", "sysSetting", "content", "version", "pluginType", "metadata", "extra", "dependPlugins", "vip", "disabled"];
const updateData: any = { id: param.id };
for (const field of editableFields) {
if (Object.prototype.hasOwnProperty.call(param, field)) {
updateData[field] = param[field];
}
}
this.normalizePluginYamlParam(updateData, "extra");
this.normalizeDependPluginsParam(updateData);
updateData.type = source.type;
updateData.author = source.author;
updateData.name = source.name;
updateData.fullName = source.fullName;
// this.normalizePluginYamlParam(param, "extra");
// this.normalizeDependPluginsParam(param);
await this.unRegisterById(param.id);
const res = await super.update(updateData);
const res = await super.update(param);
await this.registerById(param.id);
return res;
}
async compile(code: string) {
const ts = await import("typescript");
return ts.transpileModule(code, {
@@ -1605,24 +1565,12 @@ export class PluginService extends BaseService<PluginEntity> {
this.normalizeStorePluginAuthor(loaded);
}
const fullName = loaded.fullName || (loaded.author && loaded.name ? `${loaded.author}/${loaded.name}` : "");
let old = await this.findOne({
const old = await this.findOne({
where: {
type: "store",
fullName,
},
});
// 兼容历史自开发插件:旧记录可能没有 fullName,切换为在线插件时按作者和名称复用。
if (!old && entityType === "store" && loaded.author && loaded.name) {
const legacyPlugin = await this.findOne({
where: {
author: loaded.author,
name: loaded.name,
},
});
if (legacyPlugin?.type === "custom" || !legacyPlugin?.type) {
old = legacyPlugin;
}
}
const metadata = {
input: loaded.input,
@@ -1647,29 +1595,24 @@ export class PluginService extends BaseService<PluginEntity> {
extra: yaml.dump(extra),
content: loaded.content,
installed: true,
latest: old?.latest ?? loaded.latest,
status: old?.status ?? loaded.status,
downloadCount: old?.downloadCount ?? loaded.downloadCount,
score: old?.score ?? loaded.score,
syncTime: old?.syncTime ?? loaded.syncTime,
syncTime: new Date().getTime(),
disabled: old?.disabled ?? false,
};
if (!pluginEntity.pluginType) {
throw new Error(`插件类型不能为空`);
}
if (!old) {
//add
const { id } = await this.add(pluginEntity);
pluginEntity.id = id;
await this.add(pluginEntity);
} else {
if (!req.override) {
throw new Error(`插件${loaded.author}/${loaded.name}已存在`);
}
pluginEntity.id = old.id;
//update
await this.update(pluginEntity);
}
//update
await this.update(pluginEntity);
return {
id: pluginEntity.id,
};
@@ -1700,4 +1643,28 @@ export class PluginService extends BaseService<PluginEntity> {
}
);
}
isNewVersion(version: string, latestVersion: string) {
if (!latestVersion) {
return false;
}
if (latestVersion === version) {
return false;
}
//分段比较
const current = version.split(".");
const latest = latestVersion.split(".");
for (let i = 0; i < current.length; i++) {
if (parseInt(latest[i]) > parseInt(current[i])) {
return true;
} else if (parseInt(latest[i]) < parseInt(current[i])) {
return false;
}
}
return false;
}
async getPluginDefine(name: string) {
return pluginRegistry.getDefine(name);
}
}
@@ -0,0 +1,94 @@
import assert from "node:assert/strict";
import esmock from "esmock";
async function createAwsClient(records: any[]) {
const sentChanges: any[] = [];
class ListResourceRecordSetsCommand {
constructor(readonly input: any) {}
}
class ChangeResourceRecordSetsCommand {
constructor(readonly input: any) {}
}
class Route53Client {
async send(command: any) {
if (command instanceof ListResourceRecordSetsCommand) {
return { ResourceRecordSets: records };
}
sentChanges.push(command.input);
return {};
}
}
const { AwsClient } = await esmock("./aws-client.js", {
"@certd/basic": { utils: { sleep: async () => {} } },
});
const client = new AwsClient({
access: {
accessKeyId: "key",
secretAccessKey: "secret",
importRuntime: async () => ({
Route53Client,
ListResourceRecordSetsCommand,
ChangeResourceRecordSetsCommand,
}),
} as any,
logger: { info() {}, error() {} } as any,
region: "us-east-1",
});
return { client, sentChanges };
}
describe("AwsClient.route53ChangeRecord", () => {
it("keeps an existing TXT challenge when adding a wildcard challenge", async () => {
const { client, sentChanges } = await createAwsClient([
{
Name: "_acme-challenge.example.com.",
Type: "TXT",
TTL: 300,
ResourceRecords: [{ Value: '"apex-token"' }],
},
]);
await client.route53ChangeRecord({
hostedZoneId: "zone-id",
fullRecord: "_acme-challenge.example.com",
type: "TXT",
value: "wildcard-token",
action: "UPSERT",
});
const change = sentChanges[0].ChangeBatch.Changes[0];
assert.equal(change.Action, "UPSERT");
assert.deepEqual(change.ResourceRecordSet.ResourceRecords, [
{ Value: '"apex-token"' },
{ Value: '"wildcard-token"' },
]);
assert.equal(change.ResourceRecordSet.TTL, 300);
});
it("keeps the wildcard TXT challenge when removing the apex challenge", async () => {
const { client, sentChanges } = await createAwsClient([
{
Name: "_acme-challenge.example.com.",
Type: "TXT",
TTL: 300,
ResourceRecords: [{ Value: '"apex-token"' }, { Value: '"wildcard-token"' }],
},
]);
await client.route53ChangeRecord({
hostedZoneId: "zone-id",
fullRecord: "_acme-challenge.example.com",
type: "TXT",
value: "apex-token",
action: "DELETE",
});
const change = sentChanges[0].ChangeBatch.Changes[0];
assert.equal(change.Action, "UPSERT");
assert.deepEqual(change.ResourceRecordSet.ResourceRecords, [{ Value: '"wildcard-token"' }]);
});
});
@@ -129,32 +129,43 @@ export class AwsClient {
}
async route53ChangeRecord(req: { hostedZoneId: string; fullRecord: string; type: string; value: string; action: "UPSERT" | "DELETE" }) {
const { ChangeResourceRecordSetsCommand } = await this.access.importRuntime("@aws-sdk/client-route-53"); // ES Modules import
// const { Route53Client, ChangeResourceRecordSetsCommand } = require("@aws-sdk/client-route-53"); // CommonJS import
// import type { Route53ClientConfig } from "@aws-sdk/client-route-53";
const recordType = req.type.toUpperCase();
const recordValue = `"${req.value}"`;
const existingRecord = await this.route53GetRecord(req.hostedZoneId, req.fullRecord, recordType);
const existingValues = existingRecord?.ResourceRecords || [];
let action: "UPSERT" | "DELETE" = "UPSERT";
let values = existingValues;
if (req.action === "UPSERT") {
if (existingValues.some(item => item.Value === recordValue)) {
return;
}
values = [...existingValues, { Value: recordValue }];
} else {
const remainingValues = existingValues.filter(item => item.Value !== recordValue);
if (remainingValues.length === existingValues.length) {
return;
}
if (remainingValues.length === 0) {
action = "DELETE";
} else {
values = remainingValues;
}
}
const { ChangeResourceRecordSetsCommand } = await this.access.importRuntime("@aws-sdk/client-route-53");
const client = await this.route53ClientGet();
const input = {
// ChangeResourceRecordSetsRequest
HostedZoneId: req.hostedZoneId, // required
HostedZoneId: req.hostedZoneId,
ChangeBatch: {
// ChangeBatch
Changes: [
// Changes // required
{
// Change
Action: req.action as any, // required
Action: action as any,
ResourceRecordSet: {
// ResourceRecordSet
Name: req.fullRecord, // required
Type: req.type.toUpperCase() as any,
ResourceRecords: [
// ResourceRecords
{
// ResourceRecord
Value: `"${req.value}"`, // required
},
],
TTL: 60,
Name: req.fullRecord,
Type: recordType as any,
ResourceRecords: values,
TTL: existingRecord?.TTL || 60,
},
},
],
@@ -166,15 +177,27 @@ export class AwsClient {
console.log("Add record successful:", JSON.stringify(response));
await utils.sleep(3000);
return response;
/*
// { // ChangeResourceRecordSetsResponse
// ChangeInfo: { // ChangeInfo
// Id: "STRING_VALUE", // required
// Status: "PENDING" || "INSYNC", // required
// SubmittedAt: new Date("TIMESTAMP"), // required
// Comment: "STRING_VALUE",
// },
// };*/
}
private async route53GetRecord(hostedZoneId: string, fullRecord: string, type: string): Promise<any> {
const { ListResourceRecordSetsCommand } = await this.access.importRuntime("@aws-sdk/client-route-53");
const client = await this.route53ClientGet();
const command = new ListResourceRecordSetsCommand({
HostedZoneId: hostedZoneId,
StartRecordName: fullRecord,
StartRecordType: type,
MaxItems: "1",
});
const response: any = await this.doRequest(() => client.send(command));
const record = response.ResourceRecordSets?.[0];
if (!record || record.Type !== type || !this.route53RecordNamesMatch(record.Name, fullRecord)) {
return undefined;
}
return record;
}
private route53RecordNamesMatch(left: string, right: string): boolean {
return left.replace(/\.$/, "").toLowerCase() === right.replace(/\.$/, "").toLowerCase();
}
async doRequest<T>(call: () => Promise<T>): Promise<T> {
@@ -44,6 +44,7 @@ export class AcmeAccountAccess extends BaseAccess {
action: "onCaTypeList",
single: true,
mode: "tags",
emitImmediate: true,
},
required: true,
mergeScript: `
@@ -143,7 +143,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
3. <b>HTTP文件验证</b>:不支持泛域名,需要配置网站文件上传(IP证书必须选它)
4. <b>多DNS提供商</b>:每个域名可以选择独立的DNS提供商
5. <b>自动匹配</b>:此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式
6. <b>DNS持久验证</b>:需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Let's Encrypt 测试环境可以申请
6. <b>DNS持久验证</b>:需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Let's Encrypt 测试环境 可以申请
`,
})
challengeType!: string;
@@ -241,7 +241,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
}else if (form.challengeType === 'dnses'){
return '给每个域名单独配置dns提供商'
}else if (form.challengeType === 'dns-persist'){
return '请先创建并校验_validation-persist TXT持久记录,校验成功后才能提交流水线;当前仅 Let\\'s Encrypt 测试环境可以申请'
return '请先创建并校验_validation-persist TXT持久记录,校验成功后才能提交流水线;当前仅 Let\\'s Encrypt 测试环境 可以申请'
}
})
}
@@ -256,12 +256,13 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
action: "onSslProviderList",
single: true,
value: "letsencrypt",
emitImmediate: false,
required: true,
helper: "Let's Encrypt:申请最简单\nGoogle:大厂光环,兼容性好,无需配置翻墙代理\nSSL.com:仅主域名和www免费,必须设置CAA记录\n自定义ACME:管理员可在「系统设置-流水线设置」中配置",
mergeScript: `return {
component:{
on: {
"selected-change": (scope)=>{
"selectedChange": (scope)=>{
// 切换颁发机构后,清空已选择的ACME账号,避免账号与颁发机构不匹配
let form = scope.form || {};
form = form.input || form.body || form;
+67
View File
@@ -0,0 +1,67 @@
const token = process.env.ATOMGIT_TOKEN;
const version = process.env.VERSION;
if (!token || !version) {
console.error("错误: 请设置 ATOMGIT_TOKEN 和 VERSION");
process.exit(1);
}
const apiBase = "https://api.atomgit.com/api/v5/repos/certd/certd";
async function request(method, url, options = {}) {
const requestUrl = new URL(url);
for (const [key, value] of Object.entries(options.params || {})) {
requestUrl.searchParams.set(key, value);
}
const response = await fetch(requestUrl, {
method,
headers: options.headers,
body: options.data ? JSON.stringify(options.data) : undefined,
});
const data = await response.json();
if (!response.ok) {
const error = new Error("HTTP " + response.status);
error.response = { data };
throw error;
}
return data;
}
async function main() {
try {
const releaseUrl = apiBase + "/releases/v" + encodeURIComponent(version);
const release = await request("GET", releaseUrl, {
params: { access_token: token },
});
console.log("Current release: " + release.tag_name + ", status: " + release.release_status);
if (release.release_status === "latest") {
console.log("Already stable, skipping");
return;
}
const updatedRelease = await request("PATCH", releaseUrl, {
headers: { "Content-Type": "application/json" },
params: { access_token: token },
data: {
name: release.name,
body: release.body,
release_status: "latest",
},
});
console.log("Done! " + version + " is now stable: status=" + updatedRelease.release_status);
} catch (error) {
if (error?.response?.data) {
console.error("请求失败:", JSON.stringify(error.response.data, null, 2));
} else {
console.error("请求失败:", error.message || error);
}
process.exit(1);
}
}
main();
+1 -1
View File
@@ -1 +1 @@
23:10
00:58
+1 -1
View File
@@ -1 +1 @@
01:33
01:17