mirror of
https://github.com/certd/certd.git
synced 2026-10-14 09:55:57 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67f3daae62 | ||
|
|
c72fa68e4c | ||
|
|
1f5cda452c | ||
|
|
5a19d2513d | ||
|
|
c0f8145615 | ||
|
|
2ad14b82fa | ||
|
|
c8bdcdc778 | ||
|
|
15d2bd8484 | ||
|
|
7d4311115c | ||
|
|
3efa0582ef | ||
|
|
7624babc99 | ||
|
|
6cf8f350f5 | ||
|
|
0e08555220 | ||
|
|
fe738fa7d4 | ||
|
|
2439f4ab02 | ||
|
|
75b2b146ce | ||
|
|
1f3c096517 | ||
|
|
d264388c00 | ||
|
|
b8eaeeaa64 | ||
|
|
dba1a486cb | ||
|
|
5408ae6ad8 | ||
|
|
f5430a8925 | ||
|
|
9cc21204b1 | ||
|
|
f911e06a69 | ||
|
|
4a6eab0ce6 | ||
|
|
1d90fadd0c | ||
|
|
1b1a5ec94d | ||
|
|
d67379f297 | ||
|
|
249b98df66 | ||
|
|
aebbb5cc7d | ||
|
|
4f020aa8a5 | ||
|
|
02426c507a | ||
|
|
ffb7ae79eb | ||
|
|
f58f2f3cd6 | ||
|
|
794abc3d7a | ||
|
|
da406014ad | ||
|
|
9842a24a50 | ||
|
|
fb21ec04a6 | ||
|
|
15b74207dd | ||
|
|
6055e9b6ba | ||
|
|
df3e0814c4 | ||
|
|
81f660dc49 | ||
|
|
f56e2a07c6 | ||
|
|
a2fce6cf0b | ||
|
|
ccb5356bfa | ||
|
|
f44084dfd2 | ||
|
|
948d76f98f | ||
|
|
6606f32678 | ||
|
|
6af12d49fc | ||
|
|
9decb49d04 | ||
|
|
69466c1c41 | ||
|
|
adf5163a79 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -75,11 +75,12 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
|
||||
- 只有真正机械化的大批量替换、格式化或生成任务才考虑脚本/工具。若必须使用临时脚本,应放在临时目录并在同一个受控步骤内完成创建、执行、删除;不要把临时脚本落在仓库里跨多步工具调用执行。
|
||||
- 不要主动运行 `pnpm install`;缺依赖、TTY、网络导致安装或测试失败时,停止尝试并说明环境问题。
|
||||
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
|
||||
- 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
|
||||
- 代码可读性优先于短写法。尽量少用三元表达式,优先使用完整的 `if/else`;复杂条件、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
|
||||
- 方法调用链不要直接塞进另一个方法参数;先用有意义的局部变量承接返回值,再传入下一步。
|
||||
- 不要在单一表达式内嵌套分支、对象构造与方法调用。优先使用清晰的 `if/else` 分支;仅在确实能降低复杂度时才提取有意义的中间变量,避免为拆分而增加阅读跳转。
|
||||
- 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。
|
||||
- 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。
|
||||
- 如无明确复用价值,不要随意新增 `normalizeXxx` 等辅助方法;优先在现有业务方法中直接处理简单兼容或字段转换逻辑,避免增加不必要的抽象层。
|
||||
|
||||
|
||||
## 测试与验证
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` 版本。
|
||||
|
||||
最新版本号:
|
||||

|
||||
|
||||
|
||||
##### 3. 镜像构建说明:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -125,9 +125,9 @@ docker-compose up -d
|
||||
|
||||
## 三、问题
|
||||
|
||||
### 1. 迁移到PG之后,所有表的Sequence会从1开始
|
||||
### 1. 迁移到PostgreSQL之后,所有表的Sequence会从1开始
|
||||
|
||||
执行如下sql 语句,批量将所有表的id自增设置到当前数据的最大值
|
||||
执行如下sql 语句,批量将所有表的id自增设置到当前数据的最大值【仅迁移到postgresql需要执行】
|
||||
```sql
|
||||
DO $$
|
||||
DECLARE
|
||||
|
||||
@@ -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` 版本。
|
||||
|
||||
最新版本号:
|
||||

|
||||
|
||||
> 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
|
||||
|
||||
### 一键脚本安装(推荐)
|
||||
|
||||
|
||||
+1
-1
@@ -9,5 +9,5 @@
|
||||
}
|
||||
},
|
||||
"npmClient": "pnpm",
|
||||
"version": "1.44.0"
|
||||
"version": "1.44.2"
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"dependencies": {
|
||||
"copyfiles": "^2.4.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"root": "link:",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"workspaces": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
00:39
|
||||
21:57
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+8
-11
@@ -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;
|
||||
|
||||
@@ -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 @@
|
||||
23:10
|
||||
00:58
|
||||
|
||||
@@ -1 +1 @@
|
||||
01:33
|
||||
01:17
|
||||
|
||||
Reference in New Issue
Block a user