Compare commits

...
16 Commits
Author SHA1 Message Date
xiaojunnuo 9f15bb8a21 v1.45.2 2026-10-18 21:19:10 +08:00
xiaojunnuo 6bbaee0d99 build: prepare to build 2026-10-18 21:07:23 +08:00
xiaojunnuo 554a20e1fc style(plugin): 调整插件卡片简单模式的卡片体最小高度
将插件卡片简单模式下的卡片体最小高度从 128px 调整为 122px,以适配布局显示
2026-10-18 21:05:47 +08:00
xiaojunnuo 60e00f8f20 refactor(ksyun): 重构金山云客户端实现并调整请求配置
新增金山云 V2 客户端类,优化请求参数处理和 Content-Type 设置,调整 access 配置中 service 和 endpoint 的顺序,移除无用的空行代码
2026-10-18 19:39:19 +08:00
xiaojunnuo 62751bee7a perf: 新增更新金山云KCM SLB证书插件 2026-10-18 17:15:16 +08:00
xiaojunnuo 1b012c791c chore(plugin): 新增插件来源切换标签页并优化相关文本说明 2026-10-18 12:57:46 +08:00
xiaojunnuo 5f83693423 perf: 支持ikuai部署证书 2026-10-18 03:34:57 +08:00
xiaojunnuo cfd384485f perf: 为PEM输入组件添加文件拖拽上传功能 2026-10-17 17:35:32 +08:00
xiaojunnuo 61ef971518 perf(baota): 宝塔站点全自动部署插件支持部署docker站点 2026-10-17 17:34:52 +08:00
xiaojunnuo 60d0872ba1 chore: 1 2026-10-16 14:57:36 +08:00
xiaojunnuo a67d5bca2d fix: 修复腾讯云CLB部署证书逻辑 2026-10-16 14:52:13 +08:00
xiaojunnuo 1e1e76b4a8 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-10-16 14:03:48 +08:00
xiaojunnuo 9d02cd84f0 build: release 2026-10-14 23:23:47 +08:00
xiaojunnuo a9806315f1 build: publish 2026-10-14 22:55:47 +08:00
xiaojunnuo 42b743d5bb build: trigger build image 2026-10-14 22:55:36 +08:00
xiaojunnuo 0874cb2223 chore(email-service): 注释plus邮件发送逻辑并更新错误提示
注释plus邮件发送的自动使用逻辑,同时更新错误提示信息,明确指引用户配置邮件服务器
2026-10-12 10:38:02 +08:00
79 changed files with 1079 additions and 244 deletions
+12 -3
View File
@@ -122,20 +122,29 @@ const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
## YAML 和脚本规范
插件始终以完整 YAML 传递和保存,脚本源码放在顶层 `content` 字段。
插件始终以完整 YAML 传递和保存,脚本源码放在顶层 `content` 字段。`content` 必须使用 YAML 字面块 `|` 保留 JavaScript 换行,禁止使用折叠块 `>`,避免语句拼接导致 `Unexpected token 'const'` 等解析错误。
- 新插件的 `author` 必须使用启动提示词中给出的插件作者,不要自行编造作者名;修改已有插件时保留原 `author`,不要改成其他作者。
- 统一使用 `await _ctx.import(...)` 引用模块。
- `"/@/..."` 表示以绝对路径引用 `server/src/` 下的模块。
- 最后返回继承目标基类的 class。
- 不使用 `import`、`export`、装饰器或独立源码文件语法。
- 使用 `this.logger` 打印插件执行日志。
- 使用 `this.ctx.http` 访问 HTTP 能力。
- 使用目标基类提供的 logger 打印插件执行日志;在线 Access 使用 `this.ctx.logger`。
- 使用 `this.ctx.http` 访问 HTTP 能力;默认返回响应体 `response.data`,需要响应头、状态码或 Cookie 时必须设置 `returnOriginRes: true`,再从 `response.data` 读取响应体。详见 `references/context-utils.md`。
- 优先复用 `this.ctx.utils` 中的现成工具,尤其是 `this.ctx.utils.hash` 的摘要和编码方法;完整分类见 `references/context-utils.md`。
- 失败时抛出 `Error`。
- 日志打印对象时必须使用 `JSON.stringify(value)`,禁止直接字符串拼接对象导致输出 `[object Object]`;需要可读日志时先构造明确的 JSON 字符串。
### 证书域名匹配与列表组件
- 需要从前置证书获取域名时,优先在 `input` 前置配置 `certDomains` 字段,使用 `cert-domains-getter`,并通过 `inputKey` 指向证书字段。
- 远程证书或资源列表如果需要按证书域名匹配,必须为每个选项提供 `domain` 字段;`domain` 可以是单个域名字符串,也可以是域名数组。只有满足此前提,才能使用 `this.ctx.utils.options.buildGroupOptions(options, this.certDomains)` 返回分组后的列表。
- 如果服务商接口不支持分页或服务端搜索,列表组件不要设置 `pager: true` 或 `search: true`;确需搜索时在插件中获取完整列表后执行本地过滤。
需要字段格式时读取 `references/online-yaml-format.md`。
需要组件示例时读取 `references/component-examples.md`。
需要按其他字段动态显隐/必填/联动时读取 `references/merge-script.md`。
需要查找 `this.ctx.utils` 工具或确认 HTTP 返回结构时读取 `references/context-utils.md`。
### 字段动态显隐(mergeScript)
@@ -17,3 +17,61 @@ Access 插件负责保存授权配置,也负责封装平台 API/SDK,供 Task
- 业务插件通过 `dependPlugins` 依赖 Access。
- `editable: false` 时不要尝试修改 Access,在业务插件内部实现必要的调用。
- 不要在日志中打印完整授权配置。
## 地址与请求复用
- `BaseAccess` 已提供通用的 `normalizeEndpoint(endpoint)`,用于校验地址并清理 URL 末尾的 `/`。Access 插件直接调用 `this.normalizeEndpoint(this.endpoint)`,不要在插件中重复实现同名方法。
- Cookie 解析优先使用 `this.ctx.http.getCookies(response)` 或 `this.ctx.http.getCookie(response, name)`;需要原始响应时为请求设置 `returnOriginRes: true`。
## TLS 证书校验
需要兼容自签名或过期 HTTPS 证书时,在 Access 表单中使用 `skipSslVerify` 开关,并将其显式传给 `this.ctx.http.request`。默认关闭校验跳过,仅用户主动开启后才跳过:
```yaml
input:
skipSslVerify:
title: 忽略 HTTPS 证书校验
required: false
component:
name: a-switch
vModel: checked
```
```js
const response = await this.ctx.http.request({
url: `${baseUrl}/api`,
method: "POST",
skipSslVerify: this.skipSslVerify === true,
});
```
## 测试按钮
通过 `api-test` 输入项绑定 `onTestRequest`,用户点击按钮后 Certd 会调用该方法。测试方法应执行轻量、只读的连通性请求,并在失败时抛出有意义的错误:
```yaml
input:
testRequest:
title: 测试
component:
name: api-test
action: onTestRequest
helper: 点击测试接口是否正常
```
```js
async onTestRequest() {
const baseUrl = this.normalizeEndpoint(this.endpoint);
const response = await this.ctx.http.request({
url: `${baseUrl}/api/status`,
method: "GET",
skipSslVerify: this.skipSslVerify === true,
});
if (!response || response.code !== 0) {
throw new Error(`连接测试失败:${response?.message || "接口返回异常"}`);
}
return "连接成功";
}
```
@@ -70,29 +70,8 @@ name: "a-auto-complete",
options: [
{ value: "cn-qingdao", label: "华北1(青岛)" },
{ value: "cn-beijing", label: "华北2(北京)" },
{ value: "cn-zhangjiakou", label: "华北3(张家口)" },
{ value: "cn-huhehaote", label: "华北5(呼和浩特)" },
{ value: "cn-wulanchabu", label: "华北6(乌兰察布)" },
{ value: "cn-hangzhou", label: "华东1(杭州)" },
{ value: "cn-shanghai", label: "华东2(上海)" },
{ value: "cn-shenzhen", label: "华南1(深圳)" },
{ value: "cn-guangzhou", label: "华南3(广州)" },
{ value: "ap-southeast-2", label: "澳大利亚(悉尼)" },
{ value: "ap-southeast-3", label: "马来西亚(吉隆坡)" },
{ value: "ap-northeast-1", label: "日本(东京)" },
{ value: "cn-chengdu", label: "西南1(成都)" },
{ value: "ap-southeast-1", label: "新加坡" },
{ value: "ap-southeast-5", label: "印度尼西亚(雅加达)" },
{ value: "cn-hongkong", label: "中国香港" },
{ value: "eu-central-1", label: "德国(法兰克福)" },
{ value: "us-east-1", label: "美国(弗吉尼亚)" },
{ value: "us-west-1", label: "美国(硅谷)" },
{ value: "eu-west-1", label: "英国(伦敦)" },
{ value: "me-east-1", label: "阿联酋(迪拜)" },
//金融云
...
{ value: "cn-beijing-finance-1", label: "华北2 金融云(邀测)" },
{ value: "cn-hangzhou-finance", label: "华东1 金融云" },
{ value: "cn-shanghai-finance-1", label: "华东2 金融云" },
{ value: "cn-shenzhen-finance-1", label: "华南1 金融云" },
],
placeholder: "集群所属大区",
@@ -0,0 +1,103 @@
# 在线插件运行时工具
在线插件实例可以通过 `this.ctx.utils` 复用 Certd 已有工具。开发前按分类查找现成方法,避免重复实现。
## 工具分类
| 分类 | 调用入口 | 常用方法 | 用途 |
| --- | --- | --- | --- |
| 哈希与编码 | `this.ctx.utils.hash` | `md5(data, digest?)`、`sha256(data, digest?)`、`base64(data)`、`base64Decode(data)`、`hmacSha256(data, digest?)`、`hmacSha256WithKey(key, data, digest?)`、`toHex(data)`、`hexToStr(data)`、`hexToNumber(data)` | 摘要、签名、Base64/Hex 编解码 |
| 字符串 | `this.ctx.utils.string` | 读取 `packages/core/basic/src/utils/util.string.ts` | 转换、截断、格式化 |
| 域名 | `this.ctx.utils.domain` | 读取 `packages/core/basic/src/utils/util.domain.ts` | 主域名和子域名处理,域名选项列表,域名匹配分组 |
| 请求 | `this.ctx.utils.request` / `this.ctx.http` | `getCookies(response)`、`getCookie(response, name)`、`download(config)` | Cookie、下载和请求辅助 |
| 文件 | `this.ctx.utils.file` | 读取 `packages/core/basic/src/utils/util.file.ts` | 文件读写 |
| Promise/延迟 | `this.ctx.utils.promises`、`this.ctx.utils.sleep` | 读取对应实现 | 重试、等待、并发 |
| 时间 | `this.ctx.utils.dayjs` | Day.js API | 时间处理 |
| ID/缓存/合并 | `this.ctx.utils.id`、`nanoid`、`cache`、`mergeUtils` | 读取对应 `util.*.ts` | ID、缓存和配置合并 |
示例:
```javascript
const passwd = this.ctx.utils.hash.md5(password);
const pass = this.ctx.utils.hash.base64(`salt_11${password}`);
```
方法签名以仓库中 `packages/core/basic/src/utils/util.*.ts` 为准,不要凭名称猜测参数顺序。
## `this.ctx.http.request` 返回结构
`this.ctx.http.request` 默认直接返回响应体,也就是 Axios 的 `response.data`:
```javascript
const body = await this.ctx.http.request({ url: "https://api.example.com/status", method: "GET" });
```
此时 `body` 不能再读取 `body.data`。如果需要响应头、状态码或 `Set-Cookie`,必须设置 `returnOriginRes: true`:
```javascript
const response = await this.ctx.http.request({
url: "https://api.example.com/login",
method: "POST",
returnOriginRes: true,
data: { username, password },
});
const body = response.data;
const setCookie = response.headers?.["set-cookie"];
const cookies = this.ctx.http.getCookies(response);
const sessionId = this.ctx.http.getCookie(response, "session_id");
```
规则:普通 API 直接使用 `response` 作为 body;需要 headers/status/Cookie 时使用 `returnOriginRes: true`,再从 `response.data` 读取 body。首页预请求和登录请求都需要 Cookie 时,两次请求都要设置该选项。`skipSslVerify: true` 仅在用户明确配置忽略证书校验时传入。
## `HttpRequestConfig` 请求参数
`this.ctx.http.request` 接受以下 Certd 扩展参数,同时兼容 Axios 的标准请求配置(如 `url`、`method`、`headers`、`params`、`data`、`timeout`、`responseType` 等):
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `skipSslVerify` | `boolean` | 为 `true` 时关闭 HTTPS 证书校验。仅在授权表单提供了明确开关且用户主动启用时传入。 |
| `skipCheckRes` | `boolean` | Certd 内部兼容参数,在线插件通常不需要设置。 |
| `logReq` | `boolean` | 是否打印请求地址和方法。默认开启;敏感请求可设为 `false`。 |
| `logParams` | `boolean` | 是否打印 URL 查询参数。默认关闭。参数中含密钥、Token 或隐私数据时保持关闭。 |
| `logRes` | `boolean` | 是否打印响应状态和响应体摘要。默认关闭。 |
| `logData` | `boolean` | 是否打印请求体。默认关闭;禁止对包含密码、Token、Cookie 或证书私钥的请求开启。 |
| `httpProxy` | `string` | 本次请求使用的 HTTP 代理地址。没有明确代理需求时不要设置。 |
| `returnOriginRes` | `boolean` | 默认返回响应体 `response.data`;设为 `true` 时返回 Axios 原始响应对象,可读取 `response.data`、`response.headers` 和 `response.status`。获取 `Set-Cookie` 时必须开启。 |
示例:
```javascript
const response = await this.ctx.http.request({
url: `${baseUrl}/Action/login`,
method: "POST",
returnOriginRes: true,
skipSslVerify: this.ignoreTls === true,
logData: false,
headers: { "Content-Type": "application/json" },
data: requestData,
});
const body = response.data;
const cookies = response.headers?.["set-cookie"];
```
## Cookie 和文件下载辅助
原始响应可传给 HTTP 客户端提供的 Cookie 辅助方法:
```javascript
const cookies = this.ctx.http.getCookies(response);
const sessionId = this.ctx.http.getCookie(response, "session_id");
```
`getCookies(response)` 返回 `{ name, value }[]`;没有 `set-cookie` 时返回空数组。`getCookie(response, name)` 找不到目标 Cookie 时返回 `null`。
下载文件可使用 `this.ctx.http.download`,参数为 `{ http, config, savePath, logger }`,`config` 遵循 `HttpRequestConfig`:
```javascript
await this.ctx.http.download({
http: this.ctx.http,
config: { url: fileUrl, method: "GET", logRes: false },
savePath,
logger: this.ctx.logger,
});
```
@@ -47,17 +47,27 @@ content: |
### `content` 规则
- `content` 必须使用 YAML 字面块 `|` 保留脚本换行,禁止使用折叠块 `>`;折叠换行可能导致多个 JavaScript 语句拼接,出现 `Unexpected token 'const'` 等解析错误。
- 统一使用 `await _ctx.import(...)` 加载模块。
- 使用 `_ctx.import("/@/...")` 以绝对路径加载 `certd-server/src/` 下的模块,`/@` 代表 `certd-server/src` 根路径。
- 需要确认模块时使用 `_ctx.logger`,插件执行过程使用 `this.logger`。
- 最后返回继承目标基类的 class。
- 不使用 `import`、`export`、装饰器或独立源码文件语法。
- 输入字段在 class 中声明为同名属性,并与 YAML 的 `input` 配置保持一致。
- HTTP 使用 `this.ctx.http`;
- HTTP 使用 `this.ctx.http`。默认 `request()` 返回响应体 `response.data`;需要 headers、status 或 Set-Cookie 时设置 `returnOriginRes: true`,此时返回原始响应对象,响应体从 `response.data` 读取。请求扩展参数 `skipSslVerify`、`skipCheckRes`、`logReq`、`logParams`、`logRes`、`logData`、`httpProxy`、`returnOriginRes` 的说明见 `context-utils.md`。
- 优先使用 `this.ctx.utils` 中已有的哈希、编码、字符串、域名和请求工具,详见 `context-utils.md`。
- 不要使用 `console.log`。
- 日志打印对象时使用 `JSON.stringify(value)`,不要直接拼接对象输出 `[object Object]`。
- 读取授权使用 `await this.getAccess(accessId)` 或目标基类规定的授权方式。
- 失败时抛出 `Error`,不要吞掉错误。
### 证书域名与列表选项
- 需要使用前置证书域名时,在列表字段之前配置 `certDomains`,组件使用 `cert-domains-getter`,并通过 `inputKey` 绑定证书字段。
- 列表选项可按证书域名匹配时,必须为每个选项设置 `domain`;该字段支持单个域名字符串或域名数组。只有配置该字段后,才使用 `this.ctx.utils.options.buildGroupOptions(options, this.certDomains)`。
- 服务商接口不支持翻页或服务端搜索时,远程选择组件不要配置 `pager: true`、`search: true`;本地搜索可在获取完整列表后过滤。
### 编辑规则
- 修改已有插件时保留 `name`、`author`、`pluginType` 和已有兼容字段。
@@ -14,6 +14,42 @@ description: 用于开发 Certd 在线 Access 插件。输出完整 YAML,conte
- 敏感字段在 input 中设置加密或密码类组件。
- `content` 中实现授权 class 和 API 方法。
## Metadata 示例
顶层 metadata 描述插件身份和展示信息。新插件应填写 `name`、`author`、`title`、`desc`、`icon`、`group`、`version` 和 `pluginType`;`fullName` 按 `author/name` 生成:
```yaml
name: DemoAccess
author: certd
fullName: certd/DemoAccess
title: Demo-授权
desc: Demo 平台授权配置
icon: clarity:plugin-line
group: other
version: 1.0.0
pluginType: access
input:
demoKeyId:
title: Key ID
required: true
component:
name: a-input
vModel: value
demoKeySecret:
title: Key Secret
required: true
encrypt: true
component:
name: a-input-password
vModel: value
testRequest:
title: 测试
component:
name: api-test
action: onTestRequest
helper: 点击测试接口是否正常
```
## `content` 模板
```javascript
@@ -29,7 +65,7 @@ return class DemoAccess extends BaseAccess {
}
async getDomainList(req) {
this.logger.info("获取域名列表", { searchKey: req.searchKey })
this.ctx.logger.info("获取域名列表", { searchKey: req.searchKey })
const res = await this.ctx.http.request({
url: "https://api.example.com/domains",
method: "GET",
@@ -56,7 +92,8 @@ return class DemoAccess extends BaseAccess {
- 所有敏感授权值只通过 `this` 和 Certd 授权上下文使用,不打印真实值。
- `onTestRequest` 应调用实际 API 方法并在失败时抛出异常。
- 对外 API 方法应统一处理分页、错误和返回字段。
- 使用 `this.logger` 或框架提供的 logger,禁止 `console.log`。
- 在线 Access 使用 `this.ctx.logger` 打印日志,禁止使用未注入的 `this.logger` 和 `console.log`。
- 优先复用 `this.ctx.utils` 中的现成工具,例如 `this.ctx.utils.hash.md5` 和 `this.ctx.utils.hash.base64`;HTTP 返回值和 Cookie 处理规则见父 Skill 的 `references/context-utils.md`。
- 一般将API接口方法封装到Access中,其他plugin调用access的方法当做client sdk使用
## 字段动态显隐
@@ -81,7 +118,6 @@ input:
name: a-input
vModel: value
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return !form.access.authType || form.access.authType === 'aksk';
@@ -94,4 +130,4 @@ input:
- 授权表单的字段值在 `form.access.xxx`(任务表单才是 `form.xxx`)。
- 历史授权没有 `authType` 字段,条件里要用 `!form.access.authType || ...` 兜底走旧逻辑。
完整用法见父 Skill 的 `references/merge-script.md`。
完整用法见父 Skill 的 `references/merge-script.md`。
@@ -13,6 +13,22 @@ description: 用于开发 Certd 在线 DNS Provider 插件。输出完整 YAML
- `input` 中配置授权选择、域名或平台所需的参数。
- `content` 中实现创建和删除 DNS 记录的 class。
## Metadata 示例
顶层 metadata 描述插件身份和展示信息。新插件应填写 `name`、`author`、`title`、`desc`、`icon`、`group`、`version` 和 `pluginType`;`fullName` 按 `author/name` 生成:
```yaml
name: DemoDnsProvider
author: certd
fullName: certd/DemoDnsProvider
title: Demo-DNS 解析
desc: Demo 平台 DNS-01 验证
icon: clarity:plugin-line
group: dns
version: 1.0.0
pluginType: dnsProvider
```
## `content` 模板
```javascript
@@ -21,10 +37,10 @@ const { DemoAccess } = await _ctx.import("/@/plugins/plugin-lib/demo/access/inde
_ctx.logger.info("DemoAccess:", DemoAccess)
return class DemoDnsProvider extends AbstractDnsProvider {
accessId
access!: DemoAccess;
async onInstance() {
this.access = await this.getAccess(this.accessId)
this.access = this.ctx.access as DemoAccess;
}
async createRecord(options) {
@@ -14,6 +14,38 @@ description: 用于开发 Certd 在线 Task 插件。输出完整 YAML,脚本
- 输入配置放在 YAML 的 `input` 字段。
- 执行脚本放在 YAML 顶层 `content` 字段。
## Metadata 示例
顶层 metadata 描述插件身份和展示信息。新插件应填写 `name`、`author`、`title`、`desc`、`icon`、`group`、`version` 和 `pluginType`;`fullName` 按 `author/name` 生成:
```yaml
name: DemoTask
author: certd
fullName: certd/DemoTask
title: Demo-执行任务
desc: 调用 Demo 平台 API 执行操作
icon: clarity:plugin-line
group: other
version: 1.0.0
pluginType: task
input:
cert:
title: 前置任务证书
required: true
component:
name: cert-select
certDomains:
title: 当前证书域名
required: true
component:
name: cert-domains-getter
accessId:
title: Demo 授权
required: true
component:
name: access-selector
type: demo/DemoAccess # 如果是在线Access插件,要写全称, author/name, 如果是本地插件,只需要写插件名即可
```
## runStrategy
@@ -26,10 +58,11 @@ description: 用于开发 Certd 在线 Task 插件。输出完整 YAML,脚本
- 0 表示成功后跳过,1 表示正常运行。
- 设置任务的默认运行策略runStrategy。
当证书申请任务,申请证书成功后,会清空后续任务状态。此时后续`runStrategy = 0` 的任务,会重新执行。
这样可以保证相同的证书不会重复部署。
在其他场景的部署任务,可能希望每次运行都检查是否需要部署,如果即将过期就部署,没有过期就跳过。
有如下三种场景:
1、证书申请任务,设置为 `default.strategy.runStrategy = 1`,表示正常运行,此时executor每次都会执行plugin的execute方法,当无需更新证书时,返回 `"skip"`。
当申请证书成功后,调用`this.clearLastStatus();`清空后续任务的成功状态,后续任务就都会重新执行。
2、直接部署任务,建议设置为 `default.strategy.runStrategy = 0`,表示成功后跳过,executor参数与状态,如果参数没有变化,且上次的状态是成功或跳过,则直接跳过该任务,这样可以保证相同的证书不会重复部署。
3、检查后再部署的任务,可能希望每次运行都检查是否需要部署,如果即将过期就部署,没有过期就跳过。
此时可以设置 `default.strategy.runStrategy = 1`,然后在`execute`方法中,如果没有过期,就返回 `"skip"`。
## `content` 模板
+13
View File
@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
### Bug Fixes
* 修复腾讯云CLB部署证书逻辑 ([a67d5bc](https://github.com/certd/certd/commit/a67d5bca2dd12e157e45ec1b8abff54efcbdfa84))
### Performance Improvements
* 为PEM输入组件添加文件拖拽上传功能 ([cfd3844](https://github.com/certd/certd/commit/cfd384485f212cdea0b08e4b35a3f19a1acfe026))
* 新增更新金山云KCM SLB证书插件 ([62751be](https://github.com/certd/certd/commit/62751bee7a5e00245bb4dd840a5f4ea35565616a))
* 支持ikuai部署证书 ([5f83693](https://github.com/certd/certd/commit/5f83693423ec5ccd08d7bb03e4fb8315dbb8b931))
* **baota:** 宝塔站点全自动部署插件支持部署docker站点 ([61ef971](https://github.com/certd/certd/commit/61ef971518eeccbc31b09c06ec1b48589929d6bb))
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
### Bug Fixes
@@ -1 +0,0 @@
export default { setSuccess: "Settings saved", editTitle: "Edit Certificate Application Template", addTitle: "Add Certificate Application Template", name: "Template Name", default: "Default", setDefault: "Set as Default", disabled: "Disabled", createTime: "Created At", config: "Configuration" };
+15
View File
@@ -3,6 +3,21 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
### Bug Fixes
* 修复批量修改证书申请参数,无法选择证书颁发机构的bug ([9d9ae64](https://github.com/certd/certd/commit/9d9ae64aff76a09d64a5be04968ff0cc3ad097a4))
* **aliyun-cert:** 阿里云订阅证书增加申请请求接口 ([949a69f](https://github.com/certd/certd/commit/949a69f0dcf929980363fc2b3d772edae696dcaf))
### Performance Improvements
* 插件市场新增同步按钮并调整搜索区域布局 ([41ca198](https://github.com/certd/certd/commit/41ca198116e1890e65973c4317acfecb32126951))
* 多语言国际化优化,插件支持国际化配置 ([8f1b38a](https://github.com/certd/certd/commit/8f1b38ad7d445473ae71d537ad8e465f2e55ebc7))
* 开放接口支持返回证书密码,需要在openKey主动开启该设置,客户端部署证书时可以直接获取密码部署 ([3e001b8](https://github.com/certd/certd/commit/3e001b8ba431eb9af8c4ea1b20118bcf5e7ebce4))
* 完善插件翻译 ([d0463ab](https://github.com/certd/certd/commit/d0463abc2607d22aba50ae3acc6584bbed6e3a0e))
* 证书申请成功通知支持批量修改 ([22715a4](https://github.com/certd/certd/commit/22715a430e24a6132482cadb6b217acc283f270c))
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Bug Fixes
+1 -1
View File
@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.45.1"
"version": "1.45.2"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/publishlab/node-acme-client/compare/v1.45.1...v1.45.2) (2026-10-18)
**Note:** Version bump only for package @certd/acme-client
## [1.45.1](https://github.com/publishlab/node-acme-client/compare/v1.45.0...v1.45.1) (2026-10-14)
**Note:** Version bump only for package @certd/acme-client
+3 -3
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.45.1",
"version": "1.45.2",
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.45.1",
"@certd/basic": "^1.45.2",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -77,5 +77,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
"gitHead": "e9b9839d79532e4961dc0de5c097f81235e557f6"
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
### Performance Improvements
* 支持ikuai部署证书 ([5f83693](https://github.com/certd/certd/commit/5f83693423ec5ccd08d7bb03e4fb8315dbb8b931))
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
**Note:** Version bump only for package @certd/basic
+1 -1
View File
@@ -1 +1 @@
22:40
21:07
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.45.1",
"version": "1.45.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -54,5 +54,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
"gitHead": "e9b9839d79532e4961dc0de5c097f81235e557f6"
}
+1
View File
@@ -59,5 +59,6 @@ export const utils = {
locker,
mitter,
amount: amountUtils,
//废弃
request,
};
@@ -1,5 +1,5 @@
import { expect } from "chai";
import { createAgent, createAxiosService, getGlobalAgents, HttpClient, isNoProxyMatched, setGlobalHeaders, setGlobalProxy } from "./util.request.js";
import { createAgent, createAxiosService, getCookie, getCookies, getGlobalAgents, HttpClient, isNoProxyMatched, setGlobalHeaders, setGlobalProxy } from "./util.request.js";
import { ILogger } from "./util.log.js";
const testLogger = {
@@ -167,9 +167,40 @@ describe("util.request", () => {
expect(isNoProxyMatched("192.168.*", { hostname: "192.169.1.10", port: "" })).to.equal(false);
expect(isNoProxyMatched("[::1]", { hostname: "::1", port: "" })).to.equal(true);
expect(isNoProxyMatched("[::1]:8443", { hostname: "::1", port: "8443" })).to.equal(true);
expect(isNoProxyMatched("api.example.com:8443", { hostname: "api.example.com", port: "8443" })).to.equal(true);
expect(isNoProxyMatched("api.example.com:8443", { hostname: "api.example.com", port: "443" })).to.equal(false);
expect(isNoProxyMatched("127.0.0.1", { hostname: "127.0.0.2", port: "" })).to.equal(false);
expect(isNoProxyMatched(".example.com", { hostname: "example.org", port: "" })).to.equal(false);
});
it("should parse cookies from array set-cookie header", () => {
const response = {
headers: {
"set-cookie": ["JSESSIONID=abc123; Path=/; HttpOnly", "token=xyz=789; Expires=Wed, 21 Oct 2026 07:28:00 GMT"],
},
};
expect(getCookies(response)).to.deep.equal([
{ name: "JSESSIONID", value: "abc123" },
{ name: "token", value: "xyz=789" },
]);
expect(getCookie(response, "JSESSIONID")).to.equal("abc123");
expect(getCookie(response, "token")).to.equal("xyz=789");
expect(getCookie(response, "not-exists")).to.equal(null);
});
it("should parse cookies from string set-cookie header", () => {
const response = {
headers: {
"set-cookie": "JSESSIONID=abc123; Path=/",
},
};
expect(getCookies(response)).to.deep.equal([{ name: "JSESSIONID", value: "abc123" }]);
});
it("should return empty cookies when set-cookie header is missing", () => {
expect(getCookies({ headers: {} })).to.deep.equal([]);
expect(getCookies({})).to.deep.equal([]);
expect(getCookies(undefined)).to.deep.equal([]);
});
});
+61 -10
View File
@@ -1,4 +1,4 @@
import axios, { AxiosHeaders, AxiosRequestConfig } from "axios";
import axios, { AxiosHeaders, AxiosRequestConfig, AxiosResponse } from "axios";
import { ILogger, logger } from "./util.log.js";
import { HttpProxyAgent } from "http-proxy-agent";
import { HttpsProxyAgent } from "https-proxy-agent";
@@ -118,7 +118,7 @@ export function getGlobalHeaders() {
/**
* @description 创建请求实例
*/
export function createAxiosService({ logger }: { logger: ILogger }) {
export function createAxiosService({ logger }: { logger: ILogger }): HttpClient {
// 创建一个 axios 实例
const service = axios.create();
@@ -344,14 +344,24 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
return Promise.reject(err);
}
);
return service;
return {
...service,
request: service.request,
getCookies,
getCookie,
download,
};
}
//@ts-ignore
export const http = createAxiosService({ logger }) as HttpClient;
//默认返回 response.data 。 可以通过 returnOriginRes 来返回原始响应。
export type HttpClientResponse<R> = any;
export type HttpRequestConfig<D = any> = {
skipSslVerify?: boolean;
skipCheckRes?: boolean;
skipCheckRes?: boolean; //非标参数,不需要让用户知道
logReq?: boolean;
logParams?: boolean;
logRes?: boolean;
@@ -359,8 +369,18 @@ export type HttpRequestConfig<D = any> = {
httpProxy?: string;
returnOriginRes?: boolean;
} & AxiosRequestConfig<D>;
export type DownloadReq = {
http: HttpClient;
config: HttpRequestConfig;
savePath: string;
logger: ILogger;
};
export type HttpClient = {
request<D = any, R = any>(config: HttpRequestConfig<D>): Promise<HttpClientResponse<R>>;
getCookies: (response: AxiosResponse) => { name: string; value: string }[];
getCookie: (response: AxiosResponse, name: string) => string | null;
download: (config: DownloadReq) => Promise<boolean>;
};
// const http_proxy_backup = process.env.HTTP_PROXY || process.env.http_proxy;
@@ -540,7 +560,7 @@ function splitNoProxyRules(noProxy?: string) {
.filter(Boolean);
}
export async function download(req: { http: HttpClient; config: HttpRequestConfig; savePath: string; logger: ILogger }) {
export async function download(req: DownloadReq): Promise<boolean> {
const { http, config, savePath, logger } = req;
return safePromise((resolve, reject) => {
http
@@ -581,13 +601,44 @@ export async function download(req: { http: HttpClient; config: HttpRequestConfi
});
}
/**
* 解析响应头中的 set-cookie
*
* Node 环境下 set-cookie 是字符串数组,每个元素是一个完整 Cookie:
* `name=value; Path=/; HttpOnly; Expires=Wed, 21 Oct 2026 07:28:00 GMT`。
* 只取分号前的 name=value,避免把 Path、Expires 等属性误解析成 Cookie;
* value 本身可能包含 "="(如 base64),所以按第一个 "=" 切分。
*/
export function getCookies(response: any): { name: string; value: string }[] {
const setCookie = response?.headers?.["set-cookie"];
if (!setCookie) {
return [];
}
const cookieList: string[] = Array.isArray(setCookie) ? setCookie : [setCookie];
const cookies: { name: string; value: string }[] = [];
for (const cookieItem of cookieList) {
const cookiePair = String(cookieItem).split(";")[0].trim();
if (!cookiePair) {
continue;
}
const equalIndex = cookiePair.indexOf("=");
if (equalIndex <= 0) {
continue;
}
const name = cookiePair.substring(0, equalIndex).trim();
const value = cookiePair.substring(equalIndex + 1).trim();
cookies.push({ name, value });
}
return cookies;
}
export function getCookie(response: any, name: string) {
const cookies = response.headers["set-cookie"];
//根据name 返回对应的cookie
const found = cookies.find((cookie: any) => cookie.includes(name));
const cookies = getCookies(response);
const found = cookies.find((cookie: any) => cookie.name === name);
if (!found) {
return null;
}
const cookie = found.split(";")[0];
return cookie.substring(cookie.indexOf("=") + 1);
return found.value;
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
### Performance Improvements
* 支持ikuai部署证书 ([5f83693](https://github.com/certd/certd/commit/5f83693423ec5ccd08d7bb03e4fb8315dbb8b931))
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
### Performance Improvements
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.45.1",
"version": "1.45.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -21,8 +21,8 @@
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.45.1",
"@certd/plus-core": "^1.45.1",
"@certd/basic": "^1.45.2",
"@certd/plus-core": "^1.45.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": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
"gitHead": "e9b9839d79532e4961dc0de5c097f81235e557f6"
}
+2
View File
@@ -49,6 +49,7 @@ export type AccessContext = {
export abstract class BaseAccess implements IAccess {
ctx!: AccessContext;
logger!: ILogger;
async importRuntime(specifier: string) {
return await getRuntimeDepsService().importRuntime(specifier, this.ctx.logger);
@@ -56,6 +57,7 @@ export abstract class BaseAccess implements IAccess {
async setCtx(ctx: AccessContext) {
this.ctx = ctx;
this.logger = ctx.logger;
}
async onRequest(req: AccessRequestHandleReq) {
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
**Note:** Version bump only for package @certd/lib-huawei
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
**Note:** Version bump only for package @certd/lib-huawei
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.45.1",
"version": "1.45.2",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -31,5 +31,5 @@
"prettier": "3.3.3",
"tslib": "^2.8.1"
},
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
"gitHead": "e9b9839d79532e4961dc0de5c097f81235e557f6"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
**Note:** Version bump only for package @certd/lib-iframe
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
**Note:** Version bump only for package @certd/lib-iframe
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.45.1",
"version": "1.45.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -37,5 +37,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
"gitHead": "e9b9839d79532e4961dc0de5c097f81235e557f6"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
**Note:** Version bump only for package @certd/jdcloud
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
**Note:** Version bump only for package @certd/jdcloud
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.45.1",
"version": "1.45.2",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -63,5 +63,5 @@
"fetch"
]
},
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
"gitHead": "e9b9839d79532e4961dc0de5c097f81235e557f6"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
**Note:** Version bump only for package @certd/lib-k8s
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
**Note:** Version bump only for package @certd/lib-k8s
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.45.1",
"version": "1.45.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -21,7 +21,7 @@
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.45.1",
"@certd/basic": "^1.45.2",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
"gitHead": "e9b9839d79532e4961dc0de5c097f81235e557f6"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
**Note:** Version bump only for package @certd/lib-server
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
### Performance Improvements
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.45.1",
"version": "1.45.2",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -29,11 +29,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.45.1",
"@certd/basic": "^1.45.1",
"@certd/pipeline": "^1.45.1",
"@certd/plugin-lib": "^1.45.1",
"@certd/plus-core": "^1.45.1",
"@certd/acme-client": "^1.45.2",
"@certd/basic": "^1.45.2",
"@certd/pipeline": "^1.45.2",
"@certd/plugin-lib": "^1.45.2",
"@certd/plus-core": "^1.45.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": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
"gitHead": "e9b9839d79532e4961dc0de5c097f81235e557f6"
}
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
**Note:** Version bump only for package @certd/midway-flyway-js
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.45.1",
"version": "1.45.2",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -52,5 +52,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
"gitHead": "e9b9839d79532e4961dc0de5c097f81235e557f6"
}
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
**Note:** Version bump only for package @certd/plugin-cert
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
**Note:** Version bump only for package @certd/plugin-cert
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.45.1",
"version": "1.45.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.45.1"
"@certd/plugin-lib": "^1.45.2"
},
"devDependencies": {
"@types/chai": "^4.3.12",
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
"gitHead": "e9b9839d79532e4961dc0de5c097f81235e557f6"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
**Note:** Version bump only for package @certd/plugin-lib
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
### Performance Improvements
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.45.1",
"version": "1.45.2",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -17,9 +17,9 @@
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/acme-client": "^1.45.1",
"@certd/basic": "^1.45.1",
"@certd/pipeline": "^1.45.1",
"@certd/acme-client": "^1.45.2",
"@certd/basic": "^1.45.2",
"@certd/pipeline": "^1.45.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": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
"gitHead": "e9b9839d79532e4961dc0de5c097f81235e557f6"
}
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
### Bug Fixes
* 修复腾讯云CLB部署证书逻辑 ([a67d5bc](https://github.com/certd/certd/commit/a67d5bca2dd12e157e45ec1b8abff54efcbdfa84))
### Performance Improvements
* 为PEM输入组件添加文件拖拽上传功能 ([cfd3844](https://github.com/certd/certd/commit/cfd384485f212cdea0b08e4b35a3f19a1acfe026))
* 新增更新金山云KCM SLB证书插件 ([62751be](https://github.com/certd/certd/commit/62751bee7a5e00245bb4dd840a5f4ea35565616a))
* 支持ikuai部署证书 ([5f83693](https://github.com/certd/certd/commit/5f83693423ec5ccd08d7bb03e4fb8315dbb8b931))
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.45.1",
"version": "1.45.2",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -104,8 +104,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.45.1",
"@certd/pipeline": "^1.45.1",
"@certd/lib-iframe": "^1.45.2",
"@certd/pipeline": "^1.45.2",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -1,13 +1,16 @@
<template>
<div class="pem-input">
<FileInput v-bind="fileInput" class="mb-5" type="primary" text="选择文件" @change="onChange" />
<div class="pem-input__dropzone" :class="{ 'pem-input__dropzone--active': isDragging }" @dragenter.prevent="onDragEnter" @dragover.prevent="onDragOver" @dragleave.prevent="onDragLeave" @drop.prevent="onDrop">
<FileInput v-bind="fileInput" class="pem-input__file" type="primary" text="选择文件" @change="onChange" />
<span>或将文件拖拽到此处</span>
</div>
<a-textarea placeholder="或直接粘贴" v-bind="textarea" :value="modelValue" @update:value="emitValue"></a-textarea>
</div>
</template>
<script setup lang="ts">
import { notification } from "ant-design-vue";
import { ref, watch, defineEmits } from "vue";
import { ref, defineEmits } from "vue";
import FileInput from "/@/components/file-input.vue";
const props = defineProps<{
@@ -17,26 +20,57 @@ const props = defineProps<{
}>();
const emit = defineEmits(["update:modelValue"]);
const isDragging = ref(false);
function readFile(file?: File) {
if (!file) {
return;
}
if (file.size > 100 * 1024) {
notification.error({
message: "文件超过100k,请选择正确的文件",
});
return;
}
const fileReader = new FileReader();
fileReader.onload = function (event: ProgressEvent<FileReader>) {
const value = event.target?.result;
if (typeof value === "string") {
emitValue(value);
}
};
fileReader.readAsText(file);
}
function onDragEnter() {
isDragging.value = true;
}
function onDragOver() {
isDragging.value = true;
}
function onDragLeave(event: DragEvent) {
const currentTarget = event.currentTarget as HTMLElement;
const relatedTarget = event.relatedTarget as Node | null;
if (!relatedTarget || !currentTarget.contains(relatedTarget)) {
isDragging.value = false;
}
}
function onDrop(event: DragEvent) {
isDragging.value = false;
readFile(event.dataTransfer?.files[0]);
}
function emitValue(value: string) {
emit("update:modelValue", value);
}
function onChange(e: any) {
const file = e.target.files[0];
const size = file.size;
if (size > 100 * 1024) {
notification.error({
message: "文件超过100k,请选择正确的文件",
});
return;
}
const fileReader = new FileReader();
fileReader.onload = function (e: any) {
const value = e.target.result;
emitValue(value);
};
fileReader.readAsText(file); // 以文本形式读取文件
readFile(e.target.files?.[0]);
}
</script>
@@ -45,5 +79,26 @@ function onChange(e: any) {
display: flex;
flex-direction: column;
gap: 5px;
&__dropzone {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
transition:
border-color 0.2s,
background-color 0.2s;
}
&__dropzone--active {
border-color: #1677ff;
background-color: #e6f4ff;
}
&__file {
margin-bottom: 0;
}
}
</style>
@@ -8,6 +8,7 @@ import { usePluginStore } from "/@/store/plugin";
export default {
name: "OutputSelector",
// inheritAttrs: false,
props: {
modelValue: {
type: String,
@@ -30,6 +31,9 @@ export default {
const pluginStore = usePluginStore();
// const attrs = ctx.attrs;
// console.log(attrs);
async function onCreate() {
await pluginStore.init();
options.value = pluginStore.group.getPreStepOutputOptions({
@@ -37,7 +37,7 @@
</template>
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { defineComponent, inject, ref, useAttrs, watch, Ref } from "vue";
import { defineComponent, inject, ref, useAttrs, watch, Ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import { PluginDefine } from "@certd/pipeline";
import { getInputFromForm } from "./utils";
@@ -81,6 +81,7 @@ const emit = defineEmits<{
}>();
function updateValue(value: any) {
debugger;
if (props.single === true) {
const last = value?.[value.length - 1];
emit("update:value", last);
@@ -103,8 +104,18 @@ function emitSelectedChange(value: any) {
emit("selected-change", props.single === true ? options[0] : options);
}
const attrs = useAttrs();
const originalAttrs = useAttrs();
const attrs = computed(() => {
const tags = {
mode: "tags",
allowClear: true,
single: false,
};
return {
...tags,
...originalAttrs,
};
});
const getCurrentPluginDefine: any = inject("getCurrentPluginDefine", () => {
return {};
});
@@ -0,0 +1,135 @@
<template>
<div class="template-textarea">
<a-textarea :value="value" v-bind="$attrs" @update:value="emit('update:value', $event)" />
<div class="template-textarea__toolbar">
<a-dropdown trigger="click">
<a-button>
<span class="template-textarea__selected">{{ selectedName }}</span>
<fs-icon icon="ion:chevron-down" class="ml-1" />
</a-button>
<template #overlay>
<a-menu @click="handleMenuClick">
<a-menu-item v-for="item in templates" :key="item.id">
<div class="template-textarea__menu-item">
<span>{{ item.name }}</span
><a-button type="link" danger size="small" @click.stop="deleteTemplate(item.id)">删除</a-button>
</div>
</a-menu-item>
<a-menu-item v-if="templates.length === 0" disabled>暂无模板</a-menu-item>
<a-menu-divider />
<a-menu-item key="save">保存当前脚本为模板</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</template>
<script setup lang="ts">
import { notification } from "ant-design-vue";
import { onMounted, ref, watch } from "vue";
import { request } from "/@/api/service";
import { useProjectStore } from "/@/store/project";
import { useFormDialog } from "/@/use/use-dialog";
import { nanoid } from "nanoid";
defineOptions({ name: "TemplateTextarea", inheritAttrs: false });
const props = withDefaults(defineProps<{ value?: string; templateType?: string }>(), { value: "", templateType: "script" });
const emit = defineEmits<{ "update:value": [value: string] }>();
const templates = ref<Array<{ id: string; name: string; content: string }>>([]);
const selected = ref<string>();
const key = "script_templates";
const { openFormDialog } = useFormDialog();
const selectedName = ref("选择脚本模板");
async function load() {
const entity = await request({ url: "/user/settings/get", method: "post", params: { key } });
try {
templates.value = entity?.setting ? JSON.parse(entity.setting)[props.templateType] || [] : [];
} catch {
templates.value = [];
}
}
async function persist() {
const entity = await request({ url: "/user/settings/get", method: "post", params: { key } });
let setting: Record<string, unknown[]> = {};
try {
setting = entity?.setting ? JSON.parse(entity.setting) : {};
} catch {
setting = {};
}
setting[props.templateType] = templates.value;
await request({
url: "/user/settings/save",
method: "post",
data: {
key,
setting: JSON.stringify(setting),
},
});
}
function handleMenuClick({ key: id }: { key: string }) {
if (id === "save") {
saveTemplate();
return;
}
const item = templates.value.find(template => template.id === id);
if (item) {
selected.value = item.id;
selectedName.value = item.name;
emit("update:value", item.content);
}
}
async function saveTemplate() {
if (!props.value?.trim()) {
notification.warning({ message: "请输入脚本内容后再保存模板" });
return;
}
await openFormDialog({
title: "保存脚本模板",
initialForm: { name: "" },
columns: { name: { title: "模板名称", type: "text", form: { rules: [{ required: true, message: "请输入模板名称" }] } } },
async onSubmit(form: { name: string }) {
const name = form.name?.trim();
if (!name) {
notification.warning({ message: "请输入模板名称" });
throw new Error("模板名称不能为空");
}
const id = nanoid();
templates.value.push({ id, name, content: props.value });
await persist();
selected.value = id;
selectedName.value = name;
notification.success({ message: "模板保存成功" });
},
});
}
async function deleteTemplate(id: string) {
templates.value = templates.value.filter(item => item.id !== id);
if (selected.value === id) {
selected.value = undefined;
selectedName.value = "选择脚本模板";
}
await persist();
notification.success({ message: "模板删除成功" });
}
watch(() => props.templateType, load);
onMounted(load);
</script>
<style lang="less">
.template-textarea__toolbar {
display: flex;
margin-top: 6px;
align-items: center;
}
.template-textarea__selected {
display: inline-block;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
}
.template-textarea__menu-item {
display: flex;
min-width: 220px;
justify-content: space-between;
align-items: center;
}
</style>
@@ -14,6 +14,7 @@ import CertInfoUpdater from "/@/views/certd/pipeline/cert-upload/index.vue";
import ApiTest from "./common/api-test.vue";
import RefreshInput from "./common/refresh-input.vue";
import ParamsShow from "./common/params-show.vue";
import TemplateTextarea from "./common/template-textarea.vue";
export * from "./cert/index.js";
export default {
install(app: any) {
@@ -35,5 +36,6 @@ export default {
app.component("DomainSelector", DomainSelector);
app.component("InputPassword", InputPassword);
app.component("ParamsShow", ParamsShow);
app.component("TemplateTextarea", TemplateTextarea);
},
};
@@ -38,9 +38,7 @@ export default {
saveSuccess: "保存成功",
cancel: "取消",
yes: "是",
no: "否",
all: "全部",
required: "此项必填",
requiredField: "此项必填",
fieldRequired: "此项必填",
field_required: "此项必填",
@@ -77,7 +77,6 @@ export default {
searchPluginMarket: "搜索插件市场",
searchLocalPlugins: "搜索本地插件",
loadingPluginMarket: "正在加载插件市场...",
noPluginFound: "没有找到插件",
triggerManually: "手动触发",
triggerSource: "触发源",
scheduledTrigger: "触发源(定时)",
@@ -31,9 +31,9 @@ export default {
authorRuleMsg: "必须为英文或数字",
titleHelper: "插件中文名称",
descriptionHelper: "插件描述",
builtIn: "内置",
builtIn: "内置插件",
custom: "自定义",
store: "市场",
store: "插件市场",
version: "版本",
pluginDependencies: "插件依赖",
pluginDependenciesHelper: "从已同步的插件市场或内置插件中选择依赖插件(默认任意版本)",
@@ -50,7 +50,7 @@ export default {
confirmToggle: "确认要",
pluginGroup: "插件分组",
pluginManagement: "插件管理",
pluginBetaWarning: "自定义插件处于BETA测试版,后续可能会有破坏性变更",
pluginBetaWarning: "没有找到你想要的插件,可以先同步插件市场,还没有可以点击下方【AI开发插件】帮你开发一个",
localPlugin: "本地插件",
pluginMarket: "插件市场",
installedStorePlugin: "插件市场",
@@ -1,28 +1,13 @@
import { request } from "/src/api/service";
import * as _ from "lodash-es";
import { PluginConfigBean, PluginSysSetting } from "/@/views/sys/plugin/api";
import { initComponentDefine } from "./define";
import { PluginConfigBean } from "/@/views/sys/plugin/api";
import { request } from "/src/api/service";
const apiPrefix = "/pi/plugin";
const defaultInputDefine = {
component: {
name: "a-input",
vModel: "modelValue",
},
};
function initPlugins(plugins: any) {
const checkedComponents = ["a-checkbox", "a-radio", "a-switch"];
for (const plugin of plugins) {
for (const key in plugin.input) {
const field = _.merge({}, defaultInputDefine, plugin.input[key]);
const componentName = field.component.name;
if (componentName.startsWith("a-")) {
if (checkedComponents.includes(componentName)) {
field.component.vModel = "checked";
} else {
field.component.vModel = "value";
}
}
const field = initComponentDefine(plugin.input[key]);
//嵌套对象
field.key = ["input", key];
if (field.required) {
@@ -0,0 +1,28 @@
import { merge } from "lodash-es";
const defaultColumn = {
component: {
name: "a-input",
},
};
const vModelMapping: Record<string, string> = {
"remote-select": "value",
RemoteSelect: "value",
"a-switch": "checked",
"a-checkbox": "checked",
"a-radio": "checked",
};
export function initComponentDefine(column: any) {
const field = merge({}, defaultColumn, column);
const componentName = field.component.name;
if (field.component.vModel == null) {
if (componentName?.startsWith("a-")) {
field.component.vModel = vModelMapping[componentName] || "value";
} else {
field.component.vModel = vModelMapping[componentName] || "modelValue";
}
}
return field;
}
@@ -1,7 +1,8 @@
import { DynamicType, FormItemProps, useMerge } from "@fast-crud/fast-crud";
import { cloneDeep, merge, set } from "lodash-es";
import { cloneDeep, merge } from "lodash-es";
import { defineStore } from "pinia";
import * as api from "./api.plugin";
import { initComponentDefine } from "./define";
import { i18n } from "/src/locales/i18n";
interface PluginState {
group?: PluginGroups;
@@ -29,17 +30,7 @@ export function resolvePluginLocale(plugin: any, locale = i18n.global.locale.val
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";
}
}
columns[key] = initComponentDefine(columns[key]);
}
if (plugin.sysSetting?.metadata) {
@@ -207,22 +198,6 @@ export class PluginGroups {
}
}
function filterInstalledPluginGroups(groups: { [key: string]: PluginGroup }) {
const filteredGroups: { [key: string]: PluginGroup } = {};
for (const [key, group] of Object.entries(groups)) {
filteredGroups[key] = {
...group,
plugins: group.plugins.filter(plugin => {
if (plugin.type !== "store") {
return true;
}
return plugin.installed === true;
}),
};
}
return filteredGroups;
}
export const usePluginStore = defineStore({
id: "app.plugin",
state: (): PluginState => ({
@@ -257,9 +232,13 @@ export const usePluginStore = defineStore({
return this.group.get(name);
},
async getPluginConfig(query: any) {
await this.init();
return await api.GetPluginConfig(query);
},
getPluginDefineSync(name: string) {
if (!this.group) {
return null;
}
return this.group.get(name);
},
},
@@ -125,6 +125,10 @@ export const useProjectStore = defineStore("app.project", () => {
return false;
}
function getProjectId() {
return isEnterprise.value ? currentProjectId.value : undefined;
}
return {
projects,
myProjects,
@@ -141,5 +145,6 @@ export const useProjectStore = defineStore("app.project", () => {
init,
$reset,
hasPermission,
getProjectId,
};
});
@@ -30,6 +30,9 @@ export default defineComponent({
const { t } = useI18n();
const statusRef = computed(() => {
const status = statusUtil.get(props.status as string);
if (!status) {
return null;
}
return { ...status, label: t(`certd.pipeline.editor.status${String(status.value).replace(/(^|_)(\w)/g, (_, _separator, letter) => letter.toUpperCase())}`) };
});
@@ -272,6 +272,8 @@ function useStepForm() {
currentStep.input[key] = pluginSysConfig.sysSetting?.input[key];
}
}
console.log("currentPluginDefine:", currentPluginDefine.value);
};
const stepSave = async (e: any) => {
@@ -140,10 +140,10 @@ const step = ref(0);
const mode = ref<"new" | "edit">(props.pluginId ? "edit" : "new");
const pluginType = ref<string>();
const pluginTypeOptions = [
{ value: "access", label: "授权插件", desc: "接入云厂商或第三方服务凭据" },
{ value: "dnsProvider", label: "DNS Provider", desc: "管理 DNS 记录并完成域名验证" },
{ value: "deploy", label: "部署插件", desc: "将证书部署到服务器或云服务" },
{ value: "notification", label: "通知插件", desc: "通过消息、邮件等方式发送通知" },
{ value: "access", label: "授权插件", desc: "接入云厂商或第三方服务凭据,接入一个新平台前一般要开发这个插件" },
{ value: "dnsProvider", label: "DNS Provider", desc: "你想要接入新的DNS校验提供商选这个" },
{ value: "deploy", label: "部署插件", desc: "将证书部署到服务器或云服务的插件" },
{ value: "notification", label: "通知插件", desc: "新的通知渠道插件" },
// { value: "addon", label: "Addon", desc: "扩展流水线的通用辅助能力" },
];
const author = ref("");
@@ -945,7 +945,7 @@ function handleVersionClick() {
&.is-simple {
.ant-card-body {
min-height: 128px;
min-height: 122px;
}
.plugin-card__desc {
@@ -303,11 +303,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
tabs: {
name: "type",
name: "pluginType",
show: true,
defaultOption: {
show: false,
},
},
form: {
onSuccess(opts: any) {
@@ -512,7 +509,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
title: t("certd.common.sourcee"),
type: "dict-select",
search: {
show: true,
show: false,
col: {
span: 3,
},
@@ -6,6 +6,18 @@
<span class="sub">{{ t("certd.sys.plugin.pluginBetaWarning") }}</span>
</div>
</template>
<a-tabs :active-key="activeSource" class="plugin-source-tabs" @change="changeSource">
<a-tab-pane key="store">
<template #tab
><span class="plugin-source-tab-label"><fs-icon icon="ion:storefront-outline" />{{ t("certd.sys.plugin.store") }}</span></template
>
</a-tab-pane>
<a-tab-pane key="builtIn">
<template #tab
><span class="plugin-source-tab-label"><fs-icon icon="ion:extension-puzzle-outline" />{{ t("certd.sys.plugin.builtIn") }}</span></template
>
</a-tab-pane>
</a-tabs>
<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">
@@ -32,6 +44,7 @@ const pluginStore = usePluginStore();
const { openFormDialog } = useFormDialog();
const lastSyncTime = ref<number>(0);
const syncLoading = ref(false);
const activeSource = ref("store");
const autoSyncInterval = 3 * 24 * 60 * 60 * 1000;
defineOptions({
name: "SysPlugin",
@@ -46,7 +59,7 @@ async function syncOnlinePlugins(options?: { showSuccess?: boolean }) {
await api.OnlinePluginSync();
const setting = await api.OnlinePluginSetting();
lastSyncTime.value = setting.lastSyncTime || Date.now();
await pluginStore.clear();
await pluginStore.reload();
crudExpose.doRefresh();
if (options?.showSuccess !== false) {
notification.success({ message: t("certd.sys.plugin.onlinePluginSyncSuccess") });
@@ -58,6 +71,11 @@ async function syncOnlinePlugins(options?: { showSuccess?: boolean }) {
const context = { lastSyncTime, syncLoading, syncOnlinePlugins };
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
function changeSource(source: string) {
activeSource.value = source;
crudExpose.setSearchFormData({ form: { type: source }, mergeForm: true });
crudExpose.doRefresh();
}
const pluginList = computed(() => {
const rows = crudBinding.value?.data || [];
return rows.map((row: any) => {
@@ -132,6 +150,7 @@ async function autoSyncOnlinePlugins() {
useMounted(async () => {
await pluginStore.init();
autoSyncOnlinePlugins();
crudExpose.setSearchFormData({ form: { type: activeSource.value }, mergeForm: true });
crudExpose.doRefresh();
});
@@ -145,6 +164,28 @@ onBeforeUnmount(() => {
</script>
<style lang="less">
.page-sys-plugin {
.plugin-source-tabs {
flex: none;
padding: 0 10px;
.plugin-source-tab-label {
display: inline-flex;
align-items: center;
gap: 6px;
}
.ant-tabs-nav {
margin-bottom: 0;
.ant-tabs-tab {
padding: 10px 4px;
}
}
.ant-tabs-content-holder {
display: none;
}
}
.fs-page-content {
display: flex;
min-height: 0;
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.45.2](https://github.com/certd/certd/compare/v1.45.1...v1.45.2) (2026-10-18)
### Bug Fixes
* 修复腾讯云CLB部署证书逻辑 ([a67d5bc](https://github.com/certd/certd/commit/a67d5bca2dd12e157e45ec1b8abff54efcbdfa84))
### Performance Improvements
* 新增更新金山云KCM SLB证书插件 ([62751be](https://github.com/certd/certd/commit/62751bee7a5e00245bb4dd840a5f4ea35565616a))
* 支持ikuai部署证书 ([5f83693](https://github.com/certd/certd/commit/5f83693423ec5ccd08d7bb03e4fb8315dbb8b931))
* **baota:** 宝塔站点全自动部署插件支持部署docker站点 ([61ef971](https://github.com/certd/certd/commit/61ef971518eeccbc31b09c06ec1b48589929d6bb))
## [1.45.1](https://github.com/certd/certd/compare/v1.45.0...v1.45.1) (2026-10-14)
### Bug Fixes
@@ -44,9 +44,10 @@ input:
script:
title: shell脚本命令
component:
name: a-textarea
name: TemplateTextarea
vModel: value
rows: 6
templateType: githubCheckRelease
placeholder: >-
# 拉取最新镜像
@@ -23,9 +23,10 @@ input:
script:
title: shell脚本命令
component:
name: a-textarea
name: TemplateTextarea
vModel: value
rows: 6
templateType: hostShellExecute
placeholder: systemctl restart nginx
helper: 注意:如果目标主机是windows,且终端是cmd,系统会自动将多行命令通过“&&”连接成一行
required: true
@@ -215,9 +215,10 @@ input:
scriptPre:
title: 前置命令
component:
name: a-textarea
name: TemplateTextarea
vModel: value
rows: 3
templateType: uploadToHost-scriptPre
placeholder: mkdir /app/ssl
helper: 上传前执行脚本命令,做上传前的准备工作
required: false
@@ -225,9 +226,10 @@ input:
script:
title: 后置命令
component:
name: a-textarea
name: TemplateTextarea
vModel: value
rows: 5
templateType: uploadToHost-script
placeholder: 'systemctl restart nginx '
helper: |-
上传后执行脚本命令,让证书生效(比如重启nginx),不填则不执行
+16 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.45.1",
"version": "1.45.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.45.1",
"@certd/basic": "^1.45.1",
"@certd/commercial-core": "^1.45.1",
"@certd/acme-client": "^1.45.2",
"@certd/basic": "^1.45.2",
"@certd/commercial-core": "^1.45.2",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.45.1",
"@certd/lib-huawei": "^1.45.1",
"@certd/lib-k8s": "^1.45.1",
"@certd/lib-server": "^1.45.1",
"@certd/midway-flyway-js": "^1.45.1",
"@certd/pipeline": "^1.45.1",
"@certd/plugin-cert": "^1.45.1",
"@certd/plugin-lib": "^1.45.1",
"@certd/plugin-plus": "^1.45.1",
"@certd/plus-core": "^1.45.1",
"@certd/jdcloud": "^1.45.2",
"@certd/lib-huawei": "^1.45.2",
"@certd/lib-k8s": "^1.45.2",
"@certd/lib-server": "^1.45.2",
"@certd/midway-flyway-js": "^1.45.2",
"@certd/pipeline": "^1.45.2",
"@certd/plugin-cert": "^1.45.2",
"@certd/plugin-lib": "^1.45.2",
"@certd/plugin-plus": "^1.45.2",
"@certd/plus-core": "^1.45.2",
"@koa/cors": "^5.0.0",
"@midwayjs/bootstrap": "3.20.11",
"@midwayjs/cache": "3.14.0",
@@ -140,6 +140,7 @@
"why-is-node-running": "^3.2.2"
},
"lazyDependencies": {
"ksyun-sdk-node": "1.5.78",
"@alicloud/fc20230330": "^4.1.7",
"@alicloud/tea-typescript": "^1.8.0",
"@alicloud/openapi-client": "^0.4.12",
@@ -154,7 +155,7 @@
"@aws-sdk/client-s3": "^3.964.0",
"@aws-sdk/client-sts": "^3.990.0",
"ali-oss": "^6.21.0",
"tencentcloud-sdk-nodejs": "^4.1.112",
"tencentcloud-sdk-nodejs": "^4.1.320",
"cos-nodejs-sdk-v5": "^2.14.6",
"@azure/arm-dns": "^5.1.0",
"@azure/identity": "^4.13.1",
@@ -74,10 +74,31 @@ export class UserSettingsController extends CrudController<UserSettingsService>
@Post("/get", { description: Constants.per.authOnly, summary: "获取用户设置" })
async get(@Query("key") key: string) {
const { projectId, userId } = await this.getProjectUserIdRead();
const entity = await this.service.getByKey(key, userId, projectId);
const userId = this.getUserId();
const entity = await this.service.getByKey(key, userId, null);
return this.ok(entity);
}
@Post("/project/save", { description: Constants.per.authOnly, summary: "保存项目设置" })
async projectSave(@Body(ALL) bean: UserSettingsEntity) {
const { projectId,userId } = await this.getProjectUserIdWrite();
bean.userId = userId;
bean.projectId = projectId;
await this.service.save(bean);
this.auditLog({ content: "保存了项目设置" });
return this.ok({});
}
@Post("/project/get", { description: Constants.per.authOnly, summary: "获取项目设置" })
async projectGet(@Query("key") key: string) {
const { projectId,userId } = await this.getProjectUserIdRead();
const entity = await this.service.getByKey(key, userId,projectId);
return this.ok(entity);
}
@Post("/grant/get", { description: Constants.per.authOnly, summary: "获取授权设置" })
async grantSettingsGet() {
const userId = this.getUserId();
@@ -90,11 +90,11 @@ export class EmailService implements IEmailService {
const emailConf = await getEmailSettings(this.sysSettingsService, this.settingsService);
if (!emailConf.host && emailConf.usePlus == null) {
if (isPlus()) {
//自动使用plus发邮件
return await this.sendByPlus(email);
}
throw new Error("邮件服务器还未设置");
// if (isPlus()) {
// //自动使用plus发邮件
// return await this.sendByPlus(email);
// }
throw new Error("邮件服务器还未设置,请先到系统管理-邮件服务器设置中配置");
}
if (emailConf.usePlus && isPlus()) {
@@ -174,7 +174,6 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
await this.deployOne(client, domain, certId);
}
}
this.logger.info("部署完成");
}
@@ -76,9 +76,10 @@ export class GithubCheckRelease extends AbstractTaskPlugin {
@TaskInput({
title: "shell脚本命令",
component: {
name: "a-textarea",
name: "TemplateTextarea",
vModel: "value",
rows: 6,
templateType: "githubCheckRelease",
placeholder: `
# 拉取最新镜像
docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
@@ -30,9 +30,10 @@ export class HostShellExecutePlugin extends AbstractTaskPlugin {
@TaskInput({
title: "shell脚本命令",
component: {
name: "a-textarea",
name: "TemplateTextarea",
vModel: "value",
rows: 6,
templateType: "hostShellExecute",
placeholder: "systemctl restart nginx",
},
helper: "注意:如果目标主机是windows,且终端是cmd,系统会自动将多行命令通过“&&”连接成一行",
@@ -229,9 +229,10 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
@TaskInput({
title: "前置命令",
component: {
name: "a-textarea",
name: "TemplateTextarea",
vModel: "value",
rows: 3,
templateType: "uploadToHost-scriptPre",
placeholder: "mkdir /app/ssl",
},
helper: "上传前执行脚本命令,做上传前的准备工作",
@@ -242,9 +243,10 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
@TaskInput({
title: "后置命令",
component: {
name: "a-textarea",
name: "TemplateTextarea",
vModel: "value",
rows: 5,
templateType: "uploadToHost-script",
placeholder: "systemctl restart nginx ",
},
helper: "上传后执行脚本命令,让证书生效(比如重启nginx),不填则不执行\n注意:sudo需要配置免密,不要使用-i这种交互式命令\n注意:如果目标主机是windows,且终端是cmd,系统会自动将多行命令通过“&&”连接成一行",
@@ -90,7 +90,6 @@ export class KsyunAccess extends BaseAccess {
this.ctx.logger.info(res);
return res;
}
async getCert(opts: { client: KsyunClient; certId: string }) {
const res = await opts.client.doRequest({
action: "GetCertificates",
@@ -114,9 +113,9 @@ export class KsyunAccess extends BaseAccess {
accessKeyId: this.accessKeyId,
secretAccessKey: this.accessKeySecret,
region: "cn-beijing-6",
service: "cdn",
endpoint: "cdn.api.ksyun.com",
logger: this.ctx.logger,
service: "cdn",
http: this.ctx.http,
});
}
@@ -21,22 +21,33 @@ export class KsyunClient {
}
async doRequest(opts: { action: string; version: string } & HttpRequestConfig) {
const method = (opts.method || "GET").toUpperCase();
const requestData = {
...(opts.data || {}),
Action: opts.action,
Version: opts.version,
};
let url = opts.url || "/";
let data: any;
if (method === "GET") {
const query = querystring.stringify(requestData);
url = `${url}${url.includes("?") ? "&" : "?"}${query}`;
} else {
data = querystring.stringify(requestData);
}
const config = this.signRequest({
method: opts.method || "GET",
url: opts.url || "/2016-09-01/domain/GetCdnDomains",
method,
url,
baseURL: `https://${this.endpoint}`,
params: opts.params,
headers: {
"X-Action": opts.action,
"X-Version": opts.version,
},
data: opts.data,
headers: { "Content-Type": "application/x-www-form-urlencoded" },
data,
});
try {
return await this.http.request({
...config,
data: opts.data,
data: config.data,
});
} catch (e) {
this.logger.error(e.request);
@@ -0,0 +1,80 @@
import { importRuntime } from "@certd/pipeline";
import { ILogger } from "@certd/basic";
import { get } from "lodash-es";
export class KsyunClientV2 {
private sdkClient: any;
private readonly accessKeyId: string;
private readonly secretAccessKey: string;
private readonly region: string;
private readonly endpoint: string;
private readonly logger: ILogger;
private readonly clientClass: any;
private readonly contentType: string;
private readonly method: string;
private readonly actionMethods: Record<string, string>;
constructor(opts: { accessKeyId: string; secretAccessKey: string; region?: string; endpoint: string; logger: ILogger; clientClass?: string; contentType?: string; method?: string; actionMethods?: Record<string, string> }) {
this.accessKeyId = opts.accessKeyId;
this.secretAccessKey = opts.secretAccessKey;
this.region = opts.region || "cn-beijing-6";
this.endpoint = opts.endpoint;
this.logger = opts.logger;
this.clientClass = opts.clientClass || "Cdn.v20160901";
this.contentType = opts.contentType || "application/x-www-form-urlencoded";
this.method = opts.method || "";
this.actionMethods = opts.actionMethods || {};
}
private async getSdkClient() {
if (!this.sdkClient) {
const sdk = await importRuntime("ksyun-sdk-node", this.logger);
const ClientClass = get(sdk.default, this.clientClass);
if (!ClientClass) {
throw new Error("未找到金山云 SDK 客户端类");
}
const httpProfile: any = {
timeout: 60,
protocol: "",
endpoint: this.endpoint,
headers: {
"Content-Type": this.contentType,
},
};
if (this.method) {
httpProfile.method = this.method;
}
this.sdkClient = new ClientClass({
credential: {
secretId: this.accessKeyId,
secretKey: this.secretAccessKey,
},
region: this.region,
httpProfile,
});
for (const [action, method] of Object.entries(this.actionMethods)) {
if (this.sdkClient._apiList?.[action]) {
this.sdkClient._apiList[action].method = method;
}
}
}
return this.sdkClient;
}
async doRequest(opts: { action: string; data?: Record<string, any>; query?: Record<string, any> }) {
const client = await this.getSdkClient();
if (opts.query) {
const apiConfig = client._apiList?.[opts.action];
if (apiConfig) {
apiConfig.config.query = { ...apiConfig.config.query, ...opts.query };
}
}
const response = await client.request(opts.action, opts.data || {});
const result = await response.json();
this.logger.info(JSON.stringify(result));
if (result?.Error) {
throw new Error(result.Error.Message || `金山云请求失败: ${opts.action}`);
}
return result;
}
}
@@ -71,7 +71,7 @@ export class BaotaAutoDeploySiteCert extends AbstractPlusTaskPlugin {
// 宝塔并发部署会导致nginx的conf错乱,用锁串行化
const lockKey = `baota-lock-${this.accessId}`;
const { result, deployedList } = await this.autoMatchedDeploy({
const firstDeployResult = await this.autoMatchedDeploy({
targetName: "宝塔站点",
// 1. 获取证书域名列表
getCertDomains: async () => {
@@ -100,7 +100,31 @@ export class BaotaAutoDeploySiteCert extends AbstractPlusTaskPlugin {
},
});
this.deployedList = deployedList;
const dockerDeployResult = await this.autoMatchedDeploy({
targetName: "宝塔Docker站点",
getCertDomains: async () => {
return this.certDomains;
},
uploadCert: async () => {
return { key: this.cert.key, crt: this.cert.crt };
},
deployOne: async (req: { target: CertTargetItem; cert: any }) => {
await this.ctx.utils.locker.execute(lockKey, async () => {
this.logger.info(`为Docker站点: ${req.target.label} 设置证书`);
const res = await client.doRequest("/mod/docker/com/set_ssl", "", {
site_name: req.target.value,
key: req.cert.key,
csr: req.cert.crt,
});
this.logger.info(res?.msg || `Docker站点 ${req.target.label} 部署证书成功`);
});
},
getDeployTargetList: async (data: PageSearch) => {
return await this.queryDockerSiteList(client);
},
});
this.deployedList = [...new Set([...firstDeployResult.deployedList, ...dockerDeployResult.deployedList])];
const result = firstDeployResult.result === "success" || dockerDeployResult.result === "success" ? "success" : "skip";
return result;
}
@@ -126,5 +150,21 @@ export class BaotaAutoDeploySiteCert extends AbstractPlusTaskPlugin {
total: options.length,
};
}
/** 从宝塔查询 Docker 站点列表,按证书域名匹配分组 */
private async queryDockerSiteList(client: BaotaClient): Promise<{ list: CertTargetItem[]; total: number }> {
const res = await client.doRequest("/mod/docker/com/get_site_list", null, {});
const all: string[] = (res?.data || []).map((item: { name: string }) => item.name);
this.logger.info(`获取到Docker站点数量: ${all.length}`);
const options: CertTargetItem[] = all.map((item: string) => ({
value: item,
label: item,
domain: item,
}));
return {
list: options,
total: options.length,
};
}
}
new BaotaAutoDeploySiteCert();
@@ -74,7 +74,7 @@ export class KuocaiCdnAccess extends BaseAccess {
throw new Error(res.data?.message);
}
const jsessionId = this.ctx.utils.request.getCookie(res, "JSESSIONID");
const jsessionId = this.ctx.utils.http.getCookie(res, "JSESSIONID");
const token = res.data?.data;
return {
jsessionId,
@@ -107,7 +107,7 @@ export class YidunRcdnAccess extends BaseAccess {
throw new Error(res.data?.message);
}
const jsessionId = this.ctx.utils.request.getCookie(res, "JSESSIONID");
const jsessionId = this.ctx.utils.http.getCookie(res, "JSESSIONID");
const token = res.data?.data;
return {
jsessionId,
@@ -1,8 +1,8 @@
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import dayjs from "dayjs";
import { TencentAccess } from "../../../plugin-lib/tencent/index.js";
import { TencentAccess, TencentSslClient } from "../../../plugin-lib/tencent/index.js";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { CertReader, createRemoteSelectInputDefine } from "@certd/plugin-lib";
const clbRegionOptions = [
// { value: "default", label: "就近地域接入(推荐,只支持非金融区)", endpoint: "clb.tencentcloudapi.com" },
@@ -165,15 +165,33 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
async execute(): Promise<void> {
const client = this.client;
if (this.cert == null){
throw new Error("证书不能为空");
}
const access = await this.getAccess<TencentAccess>(this.accessId);
let tencentCertId = this.cert as string;
if (typeof this.cert !== "string") {
const sslClient = new TencentSslClient({
access,
logger: this.logger,
});
const certReader = new CertReader(this.cert);
tencentCertId = await sslClient.uploadToTencent({
certName: certReader.buildCertName(),
cert: this.cert,
});
}
if (!this.domain || this.domain.length === 0) {
await this.updateListener(client);
await this.updateListener(client,tencentCertId);
} else {
const domains = Array.isArray(this.domain) ? this.domain : [this.domain];
for (const domain of domains) {
this.logger.info(`开始更新域名证书:${domain},请确保已经开启了sni`);
// const lastCertId = await this.getCertIdFromProps(client, domain);
await this.updateByDomainAttr(client, domain);
await this.updateByDomainAttr(client, domain,tencentCertId);
// 不要做检查,相同的证书,不会生成新的证书id
// const checkDeployed = async (wait = 5) => {
@@ -228,16 +246,16 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
return certId;
}
async updateListener(client: any) {
const params = this.buildProps();
async updateListener(client: any, tencentCertId :string) {
const params = this.buildProps(tencentCertId);
const ret = await client.ModifyListener(params);
this.checkRet(ret);
this.logger.info("设置腾讯云CLB证书成功:", ret.RequestId, "->loadBalancerId:", this.loadBalancerId, "listenerId", this.listenerId);
return ret;
}
async updateByDomainAttr(client: any, domain) {
const params: any = this.buildProps();
async updateByDomainAttr(client: any, domain:any,tencentCertId:string) {
const params: any = this.buildProps(tencentCertId);
params.Domain = domain;
const ret = await client.ModifyDomainAttributes(params);
@@ -266,29 +284,15 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
}
return name + "-" + dayjs().format("YYYYMMDD-HHmmss");
}
buildProps() {
const certId = this.cert as string;
const certInfo = this.cert as CertInfo;
if (typeof this.cert === "string") {
return {
buildProps(tencentCertId:string) {
return {
Certificate: {
SSLMode: "UNIDIRECTIONAL", // 单向认证
CertId: certId,
CertId: tencentCertId,
},
LoadBalancerId: this.loadBalancerId,
ListenerId: this.listenerId,
};
}
return {
Certificate: {
SSLMode: "UNIDIRECTIONAL", // 单向认证
CertName: this.appendTimeSuffix(this.certName || "certd"),
CertKey: certInfo.key,
CertContent: certInfo.crt,
},
LoadBalancerId: this.loadBalancerId,
ListenerId: this.listenerId,
};
}
async getCLBList(client: any) {
+1 -1
View File
@@ -1 +1 @@
01:00
22:55
+1 -1
View File
@@ -1 +1 @@
02:47
23:23