mirror of
https://github.com/certd/certd.git
synced 2026-10-25 18:13:00 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f15bb8a21 | ||
|
|
6bbaee0d99 | ||
|
|
554a20e1fc | ||
|
|
60e00f8f20 | ||
|
|
62751bee7a | ||
|
|
1b012c791c | ||
|
|
5f83693423 | ||
|
|
cfd384485f | ||
|
|
61ef971518 | ||
|
|
60d0872ba1 | ||
|
|
a67d5bca2d | ||
|
|
1e1e76b4a8 | ||
|
|
9d02cd84f0 | ||
|
|
a9806315f1 | ||
|
|
42b743d5bb | ||
|
|
0874cb2223 |
@@ -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` 模板
|
||||
|
||||
@@ -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" };
|
||||
@@ -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
@@ -9,5 +9,5 @@
|
||||
}
|
||||
},
|
||||
"npmClient": "pnpm",
|
||||
"version": "1.45.1"
|
||||
"version": "1.45.2"
|
||||
}
|
||||
|
||||
@@ -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,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"
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
22:40
|
||||
21:07
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())}`) };
|
||||
});
|
||||
|
||||
|
||||
+2
@@ -272,6 +272,8 @@ function useStepForm() {
|
||||
currentStep.input[key] = pluginSysConfig.sysSetting?.input[key];
|
||||
}
|
||||
}
|
||||
|
||||
console.log("currentPluginDefine:", currentPluginDefine.value);
|
||||
};
|
||||
|
||||
const stepSave = async (e: any) => {
|
||||
|
||||
+4
-4
@@ -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;
|
||||
|
||||
@@ -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),不填则不执行
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+42
-2
@@ -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 @@
|
||||
01:00
|
||||
22:55
|
||||
|
||||
@@ -1 +1 @@
|
||||
02:47
|
||||
23:23
|
||||
|
||||
Reference in New Issue
Block a user