Compare commits

..

7 Commits

Author SHA1 Message Date
xiaojunnuo 2e19dda72e fix(pipeline): 修复批量随机修改定时没有生效的bug 2026-06-03 10:54:53 +08:00
xiaojunnuo c637985575 perf: 插件全局配置支持下拉选项自定义映射功能 2026-06-01 09:31:38 +08:00
xiaojunnuo 55f75c6051 chore(acme): 包装DNS提供商错误信息并抛出异常
为DNS解析记录创建操作添加异常捕获,将错误信息添加上提供商名称前缀后重新抛出,便于定位具体出错的DNS服务商
2026-05-27 10:56:08 +08:00
xiaojunnuo 42d9c3ef14 chore: 优化http提示 2026-05-27 09:19:07 +08:00
xiaojunnuo 03ce030754 build: release 2026-05-26 15:40:34 +08:00
xiaojunnuo f2c1e362a0 build: publish 2026-05-26 14:02:47 +08:00
xiaojunnuo 6426aa57a2 build: trigger build image 2026-05-26 14:02:35 +08:00
28 changed files with 210 additions and 47 deletions
+1
View File
@@ -135,6 +135,7 @@ export default defineConfig({
{text: "支付宝配置", link: "/guide/use/comm/payments/alipay.md"},
{text: "微信支付配置", link: "/guide/use/comm/payments/wxpay.md"},
{text: "彩虹易支付配置", link: "/guide/use/comm/payments/yizhifu.md"},
{text: "插件选项映射", link: "/guide/use/comm/plugin/"},
]
},
{
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.5](https://github.com/certd/certd/compare/v1.40.4...v1.40.5) (2026-05-26)
### Bug Fixes
* 安装glibc,增加Alpine镜像下 dns解析结果的兼容性 ([1a08bd3](https://github.com/certd/certd/commit/1a08bd340e1e7d3f9acf5d40f7bba7998459b8fb))
* 修复阿里云证书订单orderid 选择出错的问题 ([af9047b](https://github.com/certd/certd/commit/af9047bf3c54ce71b11727ccc6220288ed1f57be))
* 修复查询阿里云cdn Dcdn 域名太多无法选择的bug ([346fb73](https://github.com/certd/certd/commit/346fb730a37e035576f5d9ea5c0d74c052b34aeb))
## [1.40.4](https://github.com/certd/certd/compare/v1.40.3...v1.40.4) (2026-05-24)
### Bug Fixes
+1 -1
View File
@@ -22,7 +22,7 @@
| 证书域名数量 | 无限制 | 无限制 | 无限制 |
| 证书流水线条数 | 无限制 | 无限制 | 无限制 |
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 | 同专业版 |
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 | 同专业版 |
| 通知 | 邮件通知、自定义webhook | 企微、钉钉、飞书、anpush、server酱等 | 同专业版 |
| 站点监控 | 限制1条 | 无限制 | 无限制 |
| 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 | 同专业版 |
| VIP群 | 无 | 可加,一对一技术支持,必要时可申请远程协助 | 商业版技术支持 |
+2 -1
View File
@@ -6,4 +6,5 @@
* [支付宝支付配置](./payments/alipay.md)
* [微信支付配置](./payments/wxpay.md)
* [彩虹易支付配置](./payments/yizhifu.md)
* [彩虹易支付配置](./payments/yizhifu.md)
* [插件选项映射](./plugin/)
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

+37
View File
@@ -0,0 +1,37 @@
# 插件选项映射
商业版可以通过插件配置,自定义插件中下拉选择框的选项显示内容。
## 适用场景
插件中部分下拉选择框的选项可能带有"免费"、"测试"等字眼,商业版运营场景下需要隐藏或改写这些文字。
## 配置方式
1. 进入"系统管理" → "插件管理"
2. 找到需要配置的插件(如 CertApply 证书申请),点击"配置"按钮
3. 在"插件参数自定义"对话框中,找到带有下拉选项的参数(如"证书颁发机构")
4. 该参数的配置行会多出一项"选项映射",点击"自定义"
### 填写映射关系
![](./images/options-1.png)
系统会列出该下拉框的所有**选项值**和**原始显示内容**:
| 选项值 | 原始显示 | 自定义显示 |
|---|---|---|
| letsencrypt | Let's Encrypt(免费,新手推荐,支持IP证书) | [输入框] |
| google | Google(免费) | [输入框] |
- "自定义显示"一列为输入框,默认 placeholder 显示原始内容
- 只需填写**需要改写**的选项,留空的选项将保持原始显示
- 例如:将 Let's Encrypt(免费,新手推荐,支持IP证书) 改写为 Let's Encrypt
### 保存生效
![](./images/options-2.png)
填写完成后保存配置,用户在创建证书流水线时看到的选项文字即会变更为自定义内容。
+1 -1
View File
@@ -76,5 +76,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "4a09cf289d3f360c830f1a203fa11baa6af20f1c"
"gitHead": "7ceb0f6306b8b5e9ab875b9f7c41cc7d56209ea4"
}
+1 -1
View File
@@ -52,5 +52,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "4a09cf289d3f360c830f1a203fa11baa6af20f1c"
"gitHead": "7ceb0f6306b8b5e9ab875b9f7c41cc7d56209ea4"
}
+1 -1
View File
@@ -49,5 +49,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "4a09cf289d3f360c830f1a203fa11baa6af20f1c"
"gitHead": "7ceb0f6306b8b5e9ab875b9f7c41cc7d56209ea4"
}
+1 -1
View File
@@ -27,5 +27,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "4a09cf289d3f360c830f1a203fa11baa6af20f1c"
"gitHead": "7ceb0f6306b8b5e9ab875b9f7c41cc7d56209ea4"
}
+1 -1
View File
@@ -34,5 +34,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "4a09cf289d3f360c830f1a203fa11baa6af20f1c"
"gitHead": "7ceb0f6306b8b5e9ab875b9f7c41cc7d56209ea4"
}
+1 -1
View File
@@ -59,5 +59,5 @@
"fetch"
]
},
"gitHead": "4a09cf289d3f360c830f1a203fa11baa6af20f1c"
"gitHead": "7ceb0f6306b8b5e9ab875b9f7c41cc7d56209ea4"
}
+1 -1
View File
@@ -36,5 +36,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "4a09cf289d3f360c830f1a203fa11baa6af20f1c"
"gitHead": "7ceb0f6306b8b5e9ab875b9f7c41cc7d56209ea4"
}
+1 -1
View File
@@ -69,5 +69,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "4a09cf289d3f360c830f1a203fa11baa6af20f1c"
"gitHead": "7ceb0f6306b8b5e9ab875b9f7c41cc7d56209ea4"
}
+1 -1
View File
@@ -49,5 +49,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "4a09cf289d3f360c830f1a203fa11baa6af20f1c"
"gitHead": "7ceb0f6306b8b5e9ab875b9f7c41cc7d56209ea4"
}
+1 -1
View File
@@ -41,5 +41,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "4a09cf289d3f360c830f1a203fa11baa6af20f1c"
"gitHead": "7ceb0f6306b8b5e9ab875b9f7c41cc7d56209ea4"
}
+1 -1
View File
@@ -61,5 +61,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "4a09cf289d3f360c830f1a203fa11baa6af20f1c"
"gitHead": "7ceb0f6306b8b5e9ab875b9f7c41cc7d56209ea4"
}
@@ -30,10 +30,9 @@
</template>
<script lang="ts" setup>
import { Ref, ref, watch, nextTick } from "vue";
import { nextTick, Ref, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { HttpRecord } from "/@/components/plugins/cert/domains-verify-plan-editor/type";
import { dict } from "@fast-crud/fast-crud";
import { Dicts } from "/@/components/plugins/lib/dicts";
defineOptions({
@@ -46,7 +46,13 @@
<div class="form-item">
<span class="label">{{ t("certd.verifyPlan.dnsAccess") }}:</span>
<span class="input">
<access-selector v-model="item.dnsProviderAccessId" size="small" :type="item.dnsProviderAccessType || item.dnsProviderType" :placeholder="t('certd.verifyPlan.pleaseSelect')" @change="onPlanChanged"></access-selector>
<access-selector
v-model="item.dnsProviderAccessId"
size="small"
:type="item.dnsProviderAccessType || item.dnsProviderType"
:placeholder="t('certd.verifyPlan.pleaseSelect')"
@change="onPlanChanged"
></access-selector>
</span>
</div>
</div>
@@ -57,6 +57,18 @@ export class PluginGroups {
for (const plugin of groups[key].plugins) {
if (plugin.sysSetting) {
merge(plugin.input, plugin.sysSetting.metadata?.input || {});
// 应用选项映射
for (const key of Object.keys(plugin.input)) {
const inputDef = plugin.input[key];
if (inputDef.optionsMapping && inputDef.component?.options) {
const mapping = inputDef.optionsMapping;
for (const opt of inputDef.component.options) {
if (mapping[opt.value] !== undefined) {
opt.label = mapping[opt.value];
}
}
}
}
}
}
}
@@ -17,9 +17,9 @@
</thead>
<tbody>
<template v-for="item in originInputs" :key="item.key">
<template v-for="prop in editableKeys" :key="prop.key">
<template v-for="prop in getEditableKeys(item)" :key="prop.key">
<tr>
<td v-if="prop.key === 'value'" class="border-t-2 p-5" rowspan="3" :class="{ 'border-t-2': prop.key === 'value' }">{{ item.title }}</td>
<td v-if="prop.key === 'value'" class="border-t-2 p-5" :rowspan="getEditableKeys(item).length" :class="{ 'border-t-2': prop.key === 'value' }">{{ item.title }}</td>
<td class="border-t p-5" :class="{ 'border-t-2': prop.key === 'value' }">{{ prop.label }}</td>
<td class="border-t p-5" :class="{ 'border-t-2': prop.key === 'value' }">
<rollbackable :value="configForm[item.key][prop.key]" @set="prop.onSet(item)" @clear="delete configForm[item.key][prop.key]">
@@ -157,6 +157,92 @@ const editableKeys = ref([
},
]);
const optionsMappingKey = {
key: "optionsMapping",
label: "选项映射",
onSet(item: any) {
configForm[item.key]["optionsMapping"] = item.optionsMapping ?? null;
},
defaultRender(item: any) {
return () => {
const mapping = item["optionsMapping"];
if (!mapping || Object.keys(mapping).length === 0) {
return <span class="text-gray-400">未设置</span>;
}
return (
<div class="options-mapping-tags">
{Object.entries(mapping).map(([key, label]: any) => (
<a-tag color="blue" size="small" class="mb-2 mr-2">
{key} {label}
</a-tag>
))}
</div>
);
};
},
editRender(item: any) {
return () => {
const options = item.component?.options || [];
if (options.length === 0) {
return <span class="text-gray-400">该组件没有预设选项</span>;
}
const onLabelChange = (optValue: string, newLabel: string) => {
const mapping = configForm[item.key]["optionsMapping"] || {};
if (newLabel) {
mapping[optValue] = newLabel;
configForm[item.key]["optionsMapping"] = { ...mapping };
} else {
delete mapping[optValue];
if (Object.keys(mapping).length > 0) {
configForm[item.key]["optionsMapping"] = { ...mapping };
} else {
delete configForm[item.key]["optionsMapping"];
}
}
};
const getLabel = (optValue: string) => {
return configForm[item.key]["optionsMapping"]?.[optValue] || "";
};
return (
<div class="options-mapping-editor">
<table class="w-full table-auto border-collapse border border-gray-300 text-sm">
<thead>
<tr class="bg-gray-50">
<th class="border border-gray-300 px-2 py-1 text-left">选项值</th>
<th class="border border-gray-300 px-2 py-1 text-left">原始显示</th>
<th class="border border-gray-300 px-2 py-1 text-left">自定义显示</th>
</tr>
</thead>
<tbody>
{options.map((opt: any) => (
<tr>
<td class="border border-gray-300 px-2 py-1">
<code class="text-xs">{opt.value}</code>
</td>
<td class="border border-gray-300 px-2 py-1 text-gray-500">{opt.label}</td>
<td class="border border-gray-300 px-2 py-1">
<a-input size="small" placeholder={opt.label} value={getLabel(opt.value)} onUpdate:value={(val: string) => onLabelChange(opt.value, val)} />
</td>
</tr>
))}
</tbody>
</table>
<div class="helper mt-1">只需填写需要自定义的选项留空则使用原始显示内容</div>
</div>
);
};
},
};
function getEditableKeys(item: any) {
if (item.component?.name === "a-select" || item.component?.name === "icon-select") {
return [...editableKeys.value, optionsMappingKey];
}
return editableKeys.value;
}
const originInputs = computed(() => {
if (!currentPlugin.value) {
return;
@@ -15,6 +15,9 @@
<div>
<a-form-item :label="t('certd.smtpDomain')" name="host" :rules="[{ required: true, message: t('certd.pleaseEnterSmtpDomain') }]">
<a-input v-model:value="formState.host" />
<div class="helper">
{{ t("certd.sendFailHelpDoc") }}<a href="https://certd.docmirror.cn/guide/use/email/" target="_blank">{{ t("certd.emailConfigHelpDoc") }}</a>
</div>
</a-form-item>
<a-form-item :label="t('certd.smtpPort')" name="port" :rules="[{ required: true, message: t('certd.pleaseEnterSmtpPort') }]">
@@ -42,7 +45,7 @@
</a-form-item>
</div>
</a-tab-pane>
<a-tab-pane key="plus" class="plus" :disabled="!settingStore.isPlus">
<a-tab-pane key="plus" class="plus" :disabled="!settingStore.isPlus" v-if="formState.usePlus">
<template #tab>
<span class="flex items-center">
{{ t("certd.useOfficialEmailServer") }}
@@ -113,7 +116,6 @@
<div class="helper">
{{ t("certd.sendFailHelpDoc") }}<a href="https://certd.docmirror.cn/guide/use/email/" target="_blank">{{ t("certd.emailConfigHelpDoc") }}</a>
</div>
<div class="helper">{{ t("certd.tryOfficialEmailServer") }}</div>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" :loading="testFormState.loading" html-type="submit">{{ t("certd.test") }}</a-button>
@@ -48,7 +48,7 @@ import { UserSuiteEntity, UserSuiteService } from "@certd/commercial-core";
import { CertInfoService } from "../../monitor/service/cert-info-service.js";
import { TaskServiceBuilder } from "./getter/task-service-getter.js";
import { nanoid } from "nanoid";
import { set } from "lodash-es";
import { cloneDeep, set } from "lodash-es";
import { executorQueue } from "@certd/lib-server";
import parser from "cron-parser";
import { ProjectService } from "../../sys/enterprise/service/project-service.js";
@@ -1152,36 +1152,40 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
});
for (const item of list) {
const pipeline = JSON.parse(item.content);
if (trigger.props === false) {
//清除trigger
pipeline.triggers = []
} else {
const start = dayjs().format("YYYY-MM-DD") + " " + trigger.randomRange[0];
let end = dayjs().format("YYYY-MM-DD") + " " + trigger.randomRange[1];
if (trigger.randomRange[1] < trigger.randomRange[0]) {
//跨天
end = dayjs().add(1, "day").format("YYYY-MM-DD") + " " + trigger.randomRange[1];
}
const startTime = dayjs(start).valueOf();
const endTime = dayjs(end).valueOf();
const triggerConf = cloneDeep(trigger);
if (trigger.random === true) {
//随机时间
const start = dayjs().format("YYYY-MM-DD") + " " + trigger.randomRange[0];
let end = dayjs().format("YYYY-MM-DD") + " " + trigger.randomRange[1];
if (trigger.randomRange[1] < trigger.randomRange[0]) {
//跨天
end = dayjs().add(1, "day").format("YYYY-MM-DD") + " " + trigger.randomRange[1];
}
const startTime = dayjs(start).valueOf();
const endTime = dayjs(end).valueOf();
const randomTime = Math.floor(Math.random() * (endTime - startTime)) + startTime;
const time = dayjs(randomTime).format(" ss:mm:HH").replaceAll(":", " ").replaceAll(" 0", " ").trim();
set(trigger, "props.cron", `${time} * * *`)
set(triggerConf, "props.cron", `${time} * * *`)
}
delete trigger.random
delete trigger.randomRange;
delete triggerConf.random
delete triggerConf.randomRange;
pipeline.triggers = [{
id: nanoid(),
title: "定时触发",
...trigger
...triggerConf
}];
}
await this.doUpdatePipelineJson(item, pipeline);
}
}
@@ -289,15 +289,22 @@ export class AcmeService {
value: recordValue,
};
this.logger.info("添加 TXT 解析记录", JSON.stringify(recordReq));
const recordRes = await dnsProvider.createRecord(recordReq);
this.logger.info("添加 TXT 解析记录成功", JSON.stringify(recordRes));
return {
recordReq,
recordRes,
dnsProvider,
challenge,
keyAuthorization,
};
try {
const recordRes = await dnsProvider.createRecord(recordReq);
this.logger.info("添加 TXT 解析记录成功", JSON.stringify(recordRes));
return {
recordReq,
recordRes,
dnsProvider,
challenge,
keyAuthorization,
};
} catch (e: any) {
//@ts-ignore
e.message = `[${dnsProvider?.constructor?.name}错误] ${e.message}`;
throw e
}
};
let dnsProvider = providers.dnsProvider;
@@ -199,8 +199,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
if(form.challengeType === 'cname' ){
return '请按照上面的提示,给要申请证书的域名添加CNAME记录,添加后,点击验证,验证成功后不要删除记录,申请和续期证书会一直用它'
}else if (form.challengeType === 'http'){
return '请按照上面的提示,给每个域名设置文件上传配置,证书申请过程中会上传校验文件到网站根目录的.well-known/acme-challenge/目录下'
}else if (form.challengeType === 'http'){
return '请按照上面的提示,给每个域名设置文件上传配置,证书申请过程中会上传校验文件到网站根目录文件夹下,请确保该校验文件可以公网http访问到'
}else if (form.challengeType === 'dnses'){
return '给每个域名单独配置dns提供商'
}
})
+1 -1
View File
@@ -1 +1 @@
00:23
14:02
+1 -1
View File
@@ -1 +1 @@
00:32
15:40