Compare commits

...
60 Commits
Author SHA1 Message Date
xiaojunnuo fcbf1045fe v1.45.0 2026-10-03 01:00:08 +08:00
xiaojunnuo f5cfe054b7 build: prepare to build 2026-10-03 00:51:15 +08:00
xiaojunnuo 5ded5ff2b6 test: 更新测试用例的断言数据和测试描述 2026-10-03 00:51:09 +08:00
xiaojunnuo 8d31346cd3 build: prepare to build 2026-10-03 00:22:38 +08:00
xiaojunnuo 44b41b6a08 chore(certd-server): 同步迁移脚本 2026-10-02 03:17:46 +08:00
xiaojunnuo 4420472f8f perf: 宝塔站点列表支持搜索和排序 2026-10-01 21:49:39 +08:00
xiaojunnuo c4497a7018 chore: 更新DNS持久验证的支持证书类型为Google证书
更新了多个位置的提示文本,将原Let's Encrypt测试环境的说明替换为Google证书,同时调整了验证方式的说明顺序和内容,还修改了插件加载模式为metadata,优化了代码中的格式细节。
2026-10-01 10:47:28 +08:00
xiaojunnuo 03f1a7bd0e chore: 修复获取证书颁发者时未处理空directoryUrl的问题
修改两处代码,在获取DNS持久化颁发者时,当directoryUrl为空时使用accountUri作为备用,避免因缺少必要参数导致的错误
2026-10-01 10:01:41 +08:00
xiaojunnuo 97d2b50074 fix: 支持google persist校验方式 2026-09-30 18:19:23 +08:00
xiaojunnuo 2d0b85b305 fix: 缺失运行时嵌套依赖时添加清除缓存提示
当加载运行时依赖时出现嵌套模块未找到错误,添加明确的错误提示,指导用户前往插件管理页面清除缓存依赖后重试。同时新增单元测试验证该提示逻辑。
2026-09-30 16:34:11 +08:00
xiaojunnuo 28052c9387 chore: clb tencent 2026-09-30 16:22:59 +08:00
xiaojunnuo c4277350bc perf(tencent clb): 修改所有CLB地域的endpoint为就近接入 2026-09-30 15:58:13 +08:00
xiaojunnuo 97160373d6 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-09-30 15:32:57 +08:00
xiaojunnuo 0ce24618a1 docs: 修正HTTPS环境变量命名为小写格式 2026-09-29 15:06:06 +08:00
xiaojunnuo f60e63fbaf perf(monitor): 站点证书监控支持手动选中触发检查 2026-09-20 23:04:49 +08:00
xiaojunnuo 2735feb330 fix(webhook): 修复模版转义换行符,避免手动上传的证书报错
1.  将webhook插件内的replaceTemplate方法迁移到stringUtils公共工具
2.  优化模板替换处理,支持转义更多控制字符和兼容非字符串变量
3.  添加对应的单元测试用例
2026-09-20 23:04:13 +08:00
xiaojunnuo b6381d79fc chore: 批量优化插件开发相关配置与文档
1.  更新gitignore,新增.tmp_chunks忽略项
2.  切换certd插件加载模式为dev
3.  修正插件示例yaml的title注释
2026-09-20 01:26:02 +08:00
xiaojunnuo 241b2e5584 perf: 网宿插件双鉴权支持 2026-09-20 01:25:52 +08:00
xiaojunnuo bb1f232f41 fix: [BUG]使用天翼云CDN部署证书时无法匹配域名
https://github.com/certd/certd/issues/805
2026-09-19 18:05:22 +08:00
xiaojunnuo f82d3a5fcc chore(monitor): 补全站点新增/导入时的IP检查相关布尔默认值
解决批量导入等场景未传ipCheck等字段导致数据库存NULL,编辑弹窗开关过不了校验的issue #803,同时新增对应单测验证默认值和显式传参的正确性
2026-09-19 18:00:15 +08:00
xiaojunnuo bbeaf617d1 perf(open-api): openkey增加用途备注
1. 新增数据库字段存储开放接口密钥备注信息
2. 多语言添加密钥备注相关文案
3. 开放密钥接口支持备注参数传递与CRUD
4. 前端页面支持备注的展示、搜索与编辑
5. 调整插件加载模式为metadata并修复代码格式
2026-09-19 17:41:02 +08:00
xiaojunnuo ea7d14c289 fix(ctyun-cdn): 修复天翼云CDN部署插件的域名选择问题
1.  添加产品类型监听,切换产品类型时重新拉取域名列表
2.  未选择产品类型时抛出明确错误提示
3.  补全域名选项的domain字段以支持证书域名匹配分组
4.  新增单元测试覆盖上述场景
2026-09-18 01:12:36 +08:00
xiaojunnuo 3272a68ff6 style(certd client page): 调整头部布局和VIP按钮展示逻辑
优化页面头部子标题区域的对齐样式,移除非Plus版本的提示文本和条件渲染逻辑,统一展示VIP按钮
2026-09-18 00:18:13 +08:00
xiaojunnuo b5715d99ff Merge branch 'v2-dev' into v2_client 2026-09-17 23:52:49 +08:00
xiaojunnuo 3f5ba14fde ci(workflow): 修正stable-release.yml里版本号示例格式
将版本号示例的v前缀移除,和输入要求保持一致
2026-09-17 23:26:32 +08:00
xiaojunnuo 07f0d3b4d6 fix(config): 修复环境变量加载逻辑并更新配置文件
1.  重构loadDotEnv方法,支持指定加载的env文件路径,增加文件存在校验
2.  跳过注释和空行的环境变量加载,添加加载日志
3.  在默认配置中加载对应环境的env文件
4.  更新开发和生产环境的env配置项
2026-09-17 23:23:35 +08:00
xiaojunnuo 3dbbe72fc1 feat(client): 新增客户端管理功能
实现了客户端管理的完整流程:
1. 新增数据库表与迁移脚本,存储客户端信息与心跳快照
2. 实现服务端心跳上报接口与管理API
3. 新增前端页面与国际化多语言支持
4. 完善客户端在线状态判定逻辑与分页查询功能
2026-09-17 22:46:03 +08:00
xiaojunnuo 7f006d1359 refactor(plugin): 迁移同步插件逻辑到页面组件并优化上下文传递
1. 将plugin crud组件中的syncOnlinePlugins方法迁移到index.vue中
2. 把syncLoading和syncOnlinePlugins加入页面上下文传递给crud组件
3. 移除crud组件中冗余的syncLoading声明
4. 调整页面初始化调用顺序
2026-09-16 00:15:16 +08:00
xiaojunnuo d30f1c4785 refactor(plugin): 优化在线插件同步逻辑与时间格式化
1.  将自动同步相关逻辑抽离到页面组件统一管理
2.  使用dayjs替代原生Date进行时间格式化
3.  移除crud.tsx中冗余的同步配置加载代码
4.  统一维护自动同步的间隔时间常量
2026-09-15 23:52:29 +08:00
xiaojunnuo 96e0122b93 build: release 2026-09-12 20:26:56 +08:00
xiaojunnuo 7786a87b21 refactor(certd-server): 移除getReleaseMode函数,改用配置注入获取发布模式
1.  从环境配置文件中注释移除certd_release_mode的注释,启用稳定发布模式
2.  在AppController和SettingsController中注入release.mode配置替代原有的全局函数获取方式
3.  移除冗余的getReleaseMode工具函数
4.  在SettingsController中添加发布模式的日志打印
2026-09-12 20:22:18 +08:00
xiaojunnuo 34476ba14f build: publish 2026-09-12 19:24:56 +08:00
xiaojunnuo 4b942385a8 build: trigger build image 2026-09-12 19:24:46 +08:00
xiaojunnuo 14809127d6 v1.44.4 2026-09-12 19:23:35 +08:00
xiaojunnuo bd8b251d6e build: prepare to build 2026-09-12 19:17:14 +08:00
xiaojunnuo 5563abb13a fix: 修复证书插件配置无法打开的bug 2026-09-12 19:13:23 +08:00
xiaojunnuo 51e83e4c38 fix: 修复添加步骤会修改所有步骤标题的bug 2026-09-12 18:51:53 +08:00
xiaojunnuo 2b1a5296fe chore(certd-client): fix step form save data overwritten issue and add no payment tip
1. 修复步骤表单保存时因复用reactive对象导致已保存步骤被覆盖的问题,深拷贝currentStep后再传出
2. 新增无支付方式时显示暂不支持在线支付的提示
2026-09-12 18:49:24 +08:00
xiaojunnuo bded368cbb perf: 新增余额管理,管理员充值功能
1. 新增余额管理页面,支持用户余额查询与充值
2. 新增余额变更记录页面,查看用户余额明细
3. 补充多语言国际化文案
4. 修复免费订单显示文案为0元而非免费
5. 隐藏审核页面的操作按钮
6. 注释掉无用的堆日志启动代码
2026-09-12 16:06:53 +08:00
xiaojunnuo 6a649aaca0 perf(acme-account-access): AcmeAccount授权允许修改eab,重新生成account
修改了三处ACME账号表单的禁用判断条件,新增判断当editAccount为true时不禁用控件,允许在编辑已有账号时修改相关配置
2026-09-12 14:44:43 +08:00
xiaojunnuo e971145686 perf(ui): 商业版套餐购买不要求设置支付方式
1. 将套餐设置页面的"需要开启支付方式"提示改为"建议"
2. 新增支付方式为空且余额不足时的红色警告提示
3. 默认开启返利余额使用并修复支付字典加载异常处理
2026-09-12 14:28:52 +08:00
xiaojunnuo bbca449f94 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-09-12 09:32:36 +08:00
xiaojunnuo e6852c2715 docs: 更新AGENTS.md编码规范说明
refactor(plugin-fastly): 简化多行API调用代码
refactor(plugin-serv
2026-09-11 19:01:29 +08:00
xiaojunnuo 47cb523307 chore(cloudflare): 优化权限说明的换行展示
修复Cloudflare插件的权限说明文本排版,将权限项用换行分开展示,提升可读性
2026-09-10 15:13:38 +08:00
xiaojunnuo cbf46b5c48 build: release 2026-09-07 00:07:49 +08:00
xiaojunnuo 7ed3105d97 build: publish 2026-09-06 23:51:23 +08:00
xiaojunnuo 61b945c08f build: trigger build image 2026-09-06 23:51:13 +08:00
xiaojunnuo cc60cd4cd1 v1.44.3 2026-09-06 23:50:28 +08:00
xiaojunnuo dade599acb build: prepare to build 2026-09-06 23:46:14 +08:00
xiaojunnuo 1070417a36 chore: 修复fastly 通配符型号转换格式的问题 2026-09-06 23:45:56 +08:00
xiaojunnuo 81f676f1ca build: prepare to build 2026-09-06 23:29:25 +08:00
xiaojunnuo 0cf0608d3f build: prepare to build 2026-09-06 23:25:52 +08:00
xiaojunnuo dac94b64f0 fix(plugin-service): 修复同步自己开发的云端插件不显示已安装状态问题
将开发者ID、VIP状态、依赖插件、AI检查状态、插件状态、应用ID和作者字段加入插件更新逻辑,完善插件信息同步的完整性
2026-09-06 23:08:31 +08:00
xiaojunnuo 8c88092235 docs: 优化docker镜像文档的排版和内容
调整了README和docker安装指南中的镜像说明部分,将原有的零散说明改为表格形式统一展示,修正了镜像地址的展示位置,补充了稳定版slim/armv7的完整镜像地址信息,清理了重复的说明内容。
2026-09-06 17:28:00 +08:00
Steven ZhuandClaude Sonnet 5 2a2e963914 perf(plugin): add Fastly custom TLS certificates integration (#797) @szhu25
* feat(plugin): add Fastly custom TLS certificates integration

Introduces a complete set of Fastly plugins for automating custom TLS certificates deployment:
- `FastlyAccess`: Handles Fastly API authentication and provides helpers for dynamic data fetching (Services, Domains, Configs).
- `FastlyUploadCert`: Uploads a new private key and certificate (2-step POST process) or updates an existing one (PATCH).
- `FastlyDeployCert`: Activates a custom TLS certificate by linking it to a TLS configuration and domain via TLS Activations API.
- `FastlyRefreshCert`: Bulk updates existing custom TLS certificates.
- `FastlyPurgeCache`: Purges all cache for a specified Fastly Service.

Includes comprehensive unit tests covering API request formats and error handling for all tasks.

* fix(fastly): drop invalid 'multiple' option from refresh-cert input define

createRemoteSelectInputDefine has no 'multiple' option; multi-select is
already its default (mode: "tags", single: false). The excess property
broke the production build (TS2353).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015dRf6EbAmwb37cpnvgBDrh

* fix(fastly): correct TLS list unwrapping, paginate, harden purge

- access: ctx.http already unwraps to the response body, so the JSON:API
  item array is at body.data, not body.data.data — getTlsConfigurations /
  getTlsDomains / getCertificates always returned [] and the selectors
  were empty. Fetch via listAllJsonApi, which also pages past Fastly's
  20-item default so accounts with more than one page are fully listed.
- purge-cache: send no body to /purge_all (auth is header-only) instead
  of an empty JSON:API object.
- upload-cert / refresh-cert: note in the helper text that Fastly requires
  the renewed cert to reuse the original private key for PATCH updates.
- test: cover the list-helper unwrapping, pagination aggregation, and the
  bare-array /service response.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015dRf6EbAmwb37cpnvgBDrh

* fix(fastly): use output-selector for deploy cert ID input

The FastlyDeployCert `certificateId` input was a free-text box whose
helper pointed users at a non-existent "advanced settings" workflow to
grab the upstream output variable. certd only surfaces a prior step's
@TaskOutput through an input whose component is `output-selector`, so the
FastlyUploadCert `fastlyCertId` output was never selectable.

Bind the input to `output-selector` with `from: ["FastlyUploadCert"]`,
matching the pattern used by plugin-aliyun deploy-to-cdn.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J

* fix(fastly): accept cert or uploaded id in deploy, share upload helpers

Rework FastlyDeployCert to mirror the AWS CloudFront pattern instead of
demanding a hand-typed / separately-uploaded certificate id:

- The `certificateId` text box (and the earlier FastlyUploadCert-only
  output-selector) is replaced by a `cert: CertInfo | string` input whose
  output-selector accepts both a cert-apply step's cert and a
  FastlyUploadCert step's `fastlyCertId`.
- execute() binds the id directly when it got a string; when it got a raw
  CertInfo it uploads to Fastly first (new optional `name` input), then
  creates the TLS activation. So a plain "apply cert -> activate" pipeline
  needs no separate upload step.

Extract the Fastly certificate create (2-step key + cert) and update
(PATCH) flows into FastlyAccess.createCertificate / updateCertificate and
reuse them from both FastlyUploadCert and FastlyDeployCert.

Tests updated to drive the plugins through a real FastlyAccess with only
doRequestApi stubbed; added a deploy case for the raw-CertInfo path.

Note: build/tests not run locally (no Node toolchain on this machine) —
needs CI / local verification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J

* fix(fastly): reuse existing key/cert when Fastly reports "already exists"

Fastly deduplicates TLS private keys (and byte-identical certificates) by
content, so re-running an upload fails with 400 "... already exists:
'<id>'". Recover that id and carry on instead of aborting:

- ensurePrivateKey(): on "already exists", parse the id from the error
  (or match an existing key by name), else throw actionable guidance.
- createCertificate(): same treatment for the /tls/certificates POST;
  when no id is recoverable, tell the user to supply the certificate id
  so the PATCH (update) path is used.
- Shared FastlyAccess.extractExistingId() helper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J

* fix(fastly): per-domain TLS activation, idempotent on renewal

The TLS 域名 / TLS 配置 selectors use createRemoteSelectInputDefine, which
defaults to multi-select ("tags"), so domainId arrived as an array and was
written straight into tls_domain.data.id -> Fastly 500. A tls_activation
also binds exactly one domain, so wildcard + apex needs one each.

- domainId -> domainIds (string[]); loop, one activation per domain.
  configurationId gets single:true; both inputs are still normalised in
  execute() (array-or-scalar, plus legacy `domainId` key).
- New FastlyAccess.deployActivation(): looks up an existing activation for
  the domain+configuration and PATCHes it onto the new certificate;
  otherwise POSTs a fresh one. Renewals no longer fail on a duplicate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J

* feat(fastly): list service domains for TLS activation, not cert SANs

The TLS 域名 selector was populated from GET /tls/domains, which only
echoes certificate SAN entries — so a wildcard cert offered "*.g0l.net",
which cannot be activated unless a service actually serves that wildcard.

onGetTlsDomainList now lists the domains configured on the active version
of each Fastly service (new FastlyAccess.getServiceDomains()), which are
the names you can actually activate (e.g. www.g0l.net, covered by the
wildcard). Cert-derived TLS domains are still appended, tagged "(来自证书)".
The field stays free-entry so an unlisted host can be typed in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFPQmZU9uvAfR9jHgaPP9J

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:20:14 +08:00
Steven ZhuandClaude Sonnet 5 1b882a2ca4 AWS ratelimit fix (#796) @szhu25
* Parse PEM chain and import certificate chain

Split the PEM in certInfo.crt into a leaf certificate and intermediate chain (using a lookbehind regex), trim the blocks, and pass the chain to ImportCertificateCommand only when present. Replace console.log with this.logger.info and log the returned CertificateArn. This ensures the leaf cert is uploaded separately from its chain and avoids sending an empty CertificateChain.

* Add AWS retry & CloudFront deployment wait

Introduce robust retry and polling helpers to handle AWS throttling and CloudFront propagation. Added AwsClient.withRetry (exponential backoff, handles common throttling errors, default 5 attempts/base 2s) and waitForDistributionDeployed (polls until distribution Status is "Deployed", default 10min timeout/15s interval). Update deploy-to-cloudfront plugin to use withRetry for Get/UpdateDistribution and importCertificate, pass AwsClient into uploadToACM, and wait for each distribution to finish deploying before continuing to avoid PreconditionFailed errors. Improves reliability when facing rate limits and global CloudFront propagation delays; adds informative logging for retry and deployment status.

* fix(aws): resolve rate limit and runtime import bugs

* fix(aws): broaden retry detection and harden CloudFront poll

- withRetry: retry on Route53 PriorRequestNotComplete, SlowDown, SDK v3
  $retryable.throttling hints and HTTP 429/503; extract detection into
  isRetryableAwsError
- withRetry: add equal jitter to backoff so concurrent pipelines don't
  retry in lockstep
- waitForDistributionDeployed: a throttle that outlives withRetry's budget
  keeps polling until the deadline instead of aborting the deploy
- test: cover withRetry retry/exhaustion/non-throttle paths

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015dRf6EbAmwb37cpnvgBDrh

* fix(aws): fix build break and make withRetry the sole retry authority

- withRetry<T = any>: default the type param so call sites passing an
  untyped SDK client (@aws-sdk/* is a lazyDependency with no build-time
  types) don't collapse the result to `unknown`. Also annotate the two
  result consumers as `any`. Fixes the TS2339 errors that broke the
  Docker build (aws-client.ts, plugin-deploy-to-cloudfront.ts).
- Set maxAttempts: 1 on every AWS SDK client (ACM/STS/Route53/CloudFront)
  so the SDK's own retry strategy no longer stacks on top of withRetry.
- isRetryableAwsError: broaden to cover what the SDK's `standard` strategy
  used to handle now that it's disabled - $retryable hint, HTTP 429 + any
  5xx, connection-level errors (ECONNRESET/ETIMEDOUT/EAI_AGAIN/...).
- waitForDistributionDeployed: a transient error that outlives withRetry's
  budget now retries on the next poll instead of aborting the whole deploy.
- test: cover 5xx, $retryable, and connection-error retry paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrMVmQyvQjes6t4DGjuBaD

* feat(aws): make CloudFront deploy-wait opt-in (default off)

Polling GetDistribution until Status=Deployed adds 3-8min of pipeline
wall-time per run for no functional gain - CloudFront finishes the
rollout on its own once UpdateDistribution returns 200. Gate it behind
a new "等待部署完成" switch, default false. Retry/backoff on the actual
mutating calls (GetDistributionConfig / UpdateDistribution / ACM import
/ Route53) is unaffected - it wraps those sends directly, not the poll.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrMVmQyvQjes6t4DGjuBaD

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:19:47 +08:00
xiaojunnuo bd9433478a fix(plugin): 修复插件配置保存与加载的相关问题
1.  修复插件配置保存时使用fullName或name作为标识的问题
2.  为证书申请插件配置补充pluginType字段
3.  新增插件配置全局加载逻辑,启动时同步数据库插件配置到内存注册表
4.  优化配置编辑器默认值与选项添加交互
5.  补充vue依赖导入与上下文注入
2026-09-05 00:27:26 +08:00
xiaojunnuo 32ade8e4a3 build: release 2026-09-03 22:16:49 +08:00
xiaojunnuo f2bcdeeb33 build: publish 2026-09-03 22:05:17 +08:00
xiaojunnuo 4099af7f5d build: trigger build image 2026-09-03 22:05:07 +08:00
166 changed files with 5522 additions and 520 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ on:
workflow_dispatch:
inputs:
version:
description: "版本号(如 v1.42.5)"
description: "版本号(如 1.42.5)"
required: true
type: string
+2 -1
View File
@@ -42,4 +42,5 @@ output/
.uploads/
.certd-plugin-history/
.tmp
.plugin-dev/
.plugin-dev/
.tmp_chunks/
+60
View File
@@ -392,6 +392,66 @@ export class OAuthDemoAccess extends BaseAccess {
}
```
## 字段动态显隐(mergeScript)
多种鉴权方式共用一个授权时,用 `@AccessInput` 上的 `mergeScript` 按所选方式显隐字段:
```typescript
@AccessInput({
title: '鉴权方式',
value: 'apikey',
component: {
name: 'a-select',
vModel: 'value',
options: [
{ label: 'AccessKey签名', value: 'aksk' },
{ label: 'ApiKey', value: 'apikey' },
],
},
required: true,
})
authType = 'apikey';
@AccessInput({
title: 'accessKeyId',
component: { name: 'a-input', vModel: 'value' },
required: true,
mergeScript: `
return {
show: ctx.compute(({form})=>{
return !form.access.authType || form.access.authType === 'aksk';
})
}
`,
})
accessKeyId = '';
```
要点:
- `mergeScript` 返回的对象会深合并进本字段定义,可动态化的属性有 `show`、`required`、`helper`、
`component` 下的属性(`options`、`placeholder` 等)以及 `component.on` 事件。
- **动态判断必须写在 `ctx.compute(({form})=>{ ... })` 回调里**。`mergeScript` 本身在插件类
定义阶段就被拼成字符串,那时表单还没打开,用参数在服务端判断字段值没有意义。
- 授权表单字段值在 `form.access.xxx`(任务表单才是 `form.xxx`)。
- 历史授权没有新字段,条件里要用 `!form.access.authType || ...` 兜底走旧逻辑,保证老授权还能编辑保存。
- 联动其他字段用 `component.on` 事件回调,回调参数是 `{ form }`:
```typescript
mergeScript: `
return {
component: {
on: {
selectedChange: (scope) => {
const form = scope.form;
form.acmeAccountAccessId = null;
},
},
},
}
`,
```
## 注意事项
1. **插件命名**:插件名称应简洁明了,反映其功能。
+26 -4
View File
@@ -9,7 +9,7 @@ description: 用于通过 Certd API 开发、修改、读取、保存和恢复
本 Skill 使用 Certd HTTP API,不使用 WebSocket,也不依赖浏览器传递代码草稿。
- 前端只生成包含需求、API 地址和认证 Token 的启动提示词。
- 前端只生成包含需求、插件作者、API 地址和认证 Token 的启动提示词。
- Agent 直接调用 Certd API 读取和保存插件 YAML。
- Agent 的进度、日志和代码修改在 Codex/Trae 中查看。
- 插件开发临时文件和修改历史统一保存在 Agent 工作区的 `.plugin-dev/` 下,不保存到浏览器或 Certd 后端。
@@ -43,15 +43,13 @@ Certd 插件按来源分为三类:
## API 认证
提示词会提供 Certd API 地址和仅限 AI 插件开发接口的受限 Token。调用 API 时使用:
提示词会提供 Certd API 地址和仅限 AI 插件开发接口的受限的短时效的 Token。调用 API 时使用:
```http
Authorization: <token>
Content-Type: application/json
```
不要把 Token 写入代码、历史摘要、日志、提交信息或插件 YAML。
所有 Certd API 请求统一使用 Node.js 的 `fetch`。不要使用 PowerShell 的 `Invoke-RestMethod`、`Invoke-WebRequest` 或 .NET HTTP 客户端发送插件 YAML/JSON;它们在 Windows 上可能造成中文乱码或使完整 YAML 导入请求长时间无响应。
Node 请求须直接读取 UTF-8 文件或在 Node 内构造 JSON,并使用 `JSON.stringify`:
@@ -118,6 +116,7 @@ const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
- 修改后保存修改摘要。
- 恢复前再次备份当前版本。
- 不上传历史文件,不保存 Token、证书、私钥或真实授权值。
- 修改完一版后立即提交更新到certd,但不要发布到市场。
详细格式见 `references/local-history.md`。
@@ -125,6 +124,7 @@ const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
插件始终以完整 YAML 传递和保存,脚本源码放在顶层 `content` 字段。
- 新插件的 `author` 必须使用启动提示词中给出的插件作者,不要自行编造作者名;修改已有插件时保留原 `author`,不要改成其他作者。
- 统一使用 `await _ctx.import(...)` 引用模块。
- `"/@/..."` 表示以绝对路径引用 `server/src/` 下的模块。
- 最后返回继承目标基类的 class。
@@ -135,6 +135,28 @@ const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
需要字段格式时读取 `references/online-yaml-format.md`。
需要组件示例时读取 `references/component-examples.md`。
需要按其他字段动态显隐/必填/联动时读取 `references/merge-script.md`。
### 字段动态显隐(mergeScript)
字段定义支持 `mergeScript`:脚本 `return` 的对象会合并进本字段,用来做「选了 A 才显示 B」这类联动。
```yaml
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return form.access.authType === 'apikey';
})
}
```
- 动态逻辑必须写在 `ctx.compute(({form})=>{ ... })` 回调里;`mergeScript` 本身在插件类定义阶段就
被拼成字符串,那时表单还没打开,用参数在 node 侧判断字段值没有任何意义。
- 授权表单字段值在 `form.access.xxx`,任务表单字段值在 `form.xxx`。
- 新增可选字段时要给历史数据兜底,例如 `!form.access.authType || form.access.authType === 'aksk'`。
完整用法(能力、可动态化的属性、事件联动、常见错误、验证方式)见 `references/merge-script.md`。
## 示例插件
@@ -1,6 +1,6 @@
name: DemoAccess
icon: logos:airflow-icon
title: Demo-授权插件示例 # 模块-插件名
title: Demo-授权插件示例 # 格式:平台-功能 ,例如: 网宿-授权 ; 阿里云-授权 ; 天翼云-授权
group: null
desc: 这只是一个示例
version: 1.0.0
@@ -1,6 +1,6 @@
name: DemoDeploy
icon: logos:amp-icon
title: Demo-部署插件示例 # 模块-插件名
title: Demo-部署插件示例 # 格式:平台-功能 ,例如: 网宿-更新CDNpro证书 ; 阿里云-上传到CAS证书管理服务 ; 天翼云-更新CDN证书
group: cdn
desc: 这仅仅是一个示例
version: 1.0.0
@@ -1,6 +1,6 @@
name: DemoDnsProvider
icon: fa-solid:frog
title: Demo-Dns提供商插件示例 # 模块-插件名
title: Demo-Dns提供商插件示例 # 格式:平台-功能 ,例如: 阿里云-DNS提供商 ;腾讯云-DNS提供商
desc: 这只是一个示例
type: custom
version: 1.0.0
@@ -0,0 +1,181 @@
# mergeScript 用法
来源:前端 `packages/ui/certd-client/src/use/use-refrence.tsx`,类型定义来自 `@fast-crud/fast-crud`。
`mergeScript` 用来让输入字段的定义**依赖当前表单里的其他字段值动态变化**,例如:
选了某个鉴权方式才显示对应字段、切换类型后清空已选值、下拉选项跟着另一个字段变。
它写成字符串放在字段定义上(内置插件放在 `@AccessInput` / `@TaskInput` 里,在线插件放在 `input.<字段>` 下):
```ts
@AccessInput({
title: "API Key",
component: { name: "a-input", vModel: "value" },
mergeScript: `
return {
show: ctx.compute(({form})=>{
//access里面 要用 form.access.[字段名]
return form.access.authType === 'apikey';
//deploy插件里面要用form.[字段名]
return form.authType === 'apikey';
})
}
`,
})
apiKey = "";
```
## 执行时机
字段定义被前端加载时,对**每一个字段**执行一次:
```js
const func = new Function('ctx', formItem.mergeScript);
const merged = func(ctx); // 字段脚本只执行这一次
merge(formItem, merged); // 返回值深合并进该字段的定义
delete formItem.mergeScript; // 执行完就删掉,最终定义里不再有 mergeScript
```
要点:
- 脚本体内**必须 `return` 一个对象**,返回的对象会深合并到本字段的定义上。
- 脚本本身只在加载字段时跑一次,**拿不到、也不应该判断用户填了什么值**。
- 真正的动态逻辑写在返回对象里的 `ctx.compute(...)` 回调中,由 fast-crud 在渲染时求值。
## ctx 提供的能力
| 能力 | 说明 |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `ctx.compute(fn)` | 最常用。返回一个响应式的计算值:`fn` 里读到的表单字段会被自动追踪,字段变化时重新计算,视图跟着刷新 |
`ctx.compute` 的回调参数是一个 context 对象,常用字段:
- `form`:当前表单数据。**这是唯一常用的一个**。
- `row`:行数据。
- `mode`:表单模式 `add` / `edit` / `view`。
- `getComponentRef(key)`:取其他字段组件的 ref。
注意:Certd 在把 `form` 传进回调前做过一次兼容处理(`form.input || form.body || form`),
所以两种表单取值路径不同:
- **授权(Access)表单**:字段值在 `form.access.xxx`(字段 key 会被加上 `access.` 前缀)。
- **任务(Task)表单**:字段值直接是 `form.xxx`。
## 可以动态化的属性
返回的对象里,任何字段定义属性都可以用 `ctx.compute`,常用的有:
| 属性 | 用途 |
| -------------------- | ------------------------------------------------------------------------------------------------- |
| `show` | 显隐(最常用) |
| `required` | 动态必填,例如 `required: ctx.compute(({form}) => form.targetType === 'ingress')` |
| `disabled` | 动态禁用,例如账号已创建后不允许再改:`disabled: ctx.compute(({form}) => !!form.access?.account)` |
| `helper` | 动态帮助文案,回调里按条件 `return` 不同字符串 |
| `value` | 动态默认值 |
| `component` 下的属性 | 动态 `options`、`placeholder`、`inputKey`、`type`、`disabled` 等 |
| `component.on` | 事件处理,见下面「联动」 |
| `col` / `order` | 布局与排序 |
只写了一层 `show` 时最简形式就是本文开头的例子。
## 联动(监听事件并改动其他字段)
`component.on` 的事件回调**不**用 `ctx.compute` 包,它本身就是回调,参数是
`{ form, $event, ... }`;`form` 同样做过 `form.input || form.body || form` 的兼容处理。
事件名按组件而定,例如 `selectedChange`。
```ts
mergeScript: `
return {
component: {
on: {
selectedChange: (scope) => {
const form = scope.form;
// 切换颁发机构后清空 ACME 账号,避免账号与机构不匹配
form.acmeAccountAccessId = null;
},
},
},
}
`,
```
另一种写法是把回调也做成计算值(需要用到组件上配置的 `onSelectedChange` / `onChange` 属性):
```ts
mergeScript: `
return {
component: {
onSelectedChange: ctx.compute(({form}) => {
return ($event) => {
form.dnsProviderAccessType = $event.accessType;
};
}),
},
}
`,
```
两种写法都能用,优先参考同目录已有插件的写法,保持一致。
## 常见错误
- **不要用 node 侧变量决定条件**。`mergeScript` 是在插件类定义阶段(node 侧)拼出来的字符串,
此时表单还没打开,拿不到任何用户输入;用参数拼条件等于把逻辑写死在服务端。
条件必须原样落到 `ctx.compute` 回调里。
- **不要用 `if` 直接判断字段值**。回调必须 `return` 一个值,写成
`show: ctx.compute(({form}) => { if (...) { return true } })` 也可以,但不要漏掉 `return`。
- **不要漏掉历史数据兼容**。新增可选字段时,历史数据里没有这个字段(值为 `undefined`),
条件要按旧行为兜底,例如 `!form.access.authType || form.access.authType === 'aksk'`。
- **不要访问 `form.access` 以外的路径**去读授权字段,也不要在授权表单里按任务表单的路径取值。
- **不要依赖脚本执行时的副作用**(打印日志、改全局状态),它在每次打开表单时都会重新执行。
## YAML 写法(在线插件)
`mergeScript` 是含换行的脚本字符串,必须用 YAML 块标量:
```yaml
input:
apiKey:
title: API Key
component:
name: a-input
vModel: value
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return form.access.authType === 'apikey';
})
}
```
导出生成的缩进比较特殊(块标量内容统一缩进),**不要手工调整缩进**:
用脚本生成 YAML,并保证导出后再导入内容一致。转换成 block scalar 时注意:
- `|` / `|2-` 等块标量不需要转义换行,不要把脚本写成带 `\n` 的单行。
- 脚本里不要出现 `??` 这类容易被误判为乱码的写法,用显式 `if (x == null)` 代替。
## 验证方式
前端表单没法在插件开发流程里直接断言,可以在后端加一个 `src/**/*.test.ts`,
按前端 `useReference` 的路径执行字段上的 `mergeScript`:
```ts
const computedGetters = [];
const ctx = {
compute: getter => {
computedGetters.push(getter);
return getter;
},
asyncCompute: () => undefined,
computed: () => undefined,
};
new Function('ctx', mergeScript)(ctx);
// 用假的 form 求值,断言显隐结果
const show = computedGetters[0]({ form: { access: { authType: 'apikey' } } });
```
参考实现:`packages/ui/certd-server/src/plugins/plugin-wangsu/access-input.test.ts`。
要点是断言「同一个 show 函数换 `authType` 结果会变」,证明显隐确实是运行时算的,
而不是在 node 侧定死的。
@@ -7,7 +7,7 @@
```yaml
name: DemoTask
author: demo
title: Demo 任务
title: Demo-插件名称 # 格式:平台-功能 ,例如: 网宿-更新CDNpro证书 ; 阿里云-上传到CAS证书管理服务 ; 天翼云-更新CDN证书
desc: 插件说明
icon: clarity:plugin-line
pluginType: task
@@ -61,6 +61,8 @@ content: |
### 编辑规则
- 修改已有插件时保留 `name`、`author`、`pluginType` 和已有兼容字段。
- 新插件的 `author` 使用启动提示词中给出的插件作者,不要自行编造作者名;启动提示词未给出作者时先向用户确认。
- `fullName` 由 `author/name` 组成,作者变化会导致插件身份变化,不要随意修改。
- 只修改需求涉及的字段,避免删除未知的 YAML 字段。
- 脚本过长时仍放在同一个 `content` block scalar 中。
- 提交前检查 YAML 可解析、`content` 非空、版本和插件类型没有被意外修改。
@@ -57,4 +57,41 @@ return class DemoAccess extends BaseAccess {
- `onTestRequest` 应调用实际 API 方法并在失败时抛出异常。
- 对外 API 方法应统一处理分页、错误和返回字段。
- 使用 `this.logger` 或框架提供的 logger,禁止 `console.log`。
- 一般将API接口方法封装到Access中,其他plugin调用access的方法当做client sdk使用
- 一般将API接口方法封装到Access中,其他plugin调用access的方法当做client sdk使用
## 字段动态显隐
多种鉴权方式(例如「ApiKey」和「AccessKey签名」)共用一个授权时,
用字段上的 `mergeScript` 按所选方式显隐对应字段,只让用户看到当前方式需要的字段:
```yaml
input:
authType:
title: 鉴权方式
value: apikey
component:
name: a-select
vModel: value
options:
- { label: AccessKey签名, value: aksk }
- { label: ApiKey, value: apikey }
accessKeyId:
title: accessKeyId
component:
name: a-input
vModel: value
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return !form.access.authType || form.access.authType === 'aksk';
})
}
```
- 判断必须写在 `ctx.compute(({form})=>{ ... })` 回调里;脚本本身在插件定义阶段就拼好了,
那时表单还没打开,用参数在服务端判断字段值没有意义。
- 授权表单的字段值在 `form.access.xxx`(任务表单才是 `form.xxx`)。
- 历史授权没有 `authType` 字段,条件里要用 `!form.access.authType || ...` 兜底走旧逻辑。
完整用法见父 Skill 的 `references/merge-script.md`。
+1 -1
View File
@@ -75,7 +75,7 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 只有真正机械化的大批量替换、格式化或生成任务才考虑脚本/工具。若必须使用临时脚本,应放在临时目录并在同一个受控步骤内完成创建、执行、删除;不要把临时脚本落在仓库里跨多步工具调用执行。
- 不要主动运行 `pnpm install`;缺依赖、TTY、网络导致安装或测试失败时,停止尝试并说明环境问题。
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
- 代码可读性优先于短写法。尽量少用三元表达式,优先使用完整的 `if/else`;复杂条件、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
- 【重要重要重要】代码可读性优先于短写法。尽量少用三元表达式,优先使用完整的 `if/else`【其他技能与此条要求冲突的,以此条要求为准】;复杂条件、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
- 方法调用链不要直接塞进另一个方法参数;先用有意义的局部变量承接返回值,再传入下一步。
- 不要在单一表达式内嵌套分支、对象构造与方法调用。优先使用清晰的 `if/else` 分支;仅在确实能降低复杂度时才提取有意义的中间变量,避免为拆分而增加阅读跳转。
- 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。
+47
View File
@@ -3,6 +3,53 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Bug Fixes
* [BUG]使用天翼云CDN部署证书时无法匹配域名 ([bb1f232](https://github.com/certd/certd/commit/bb1f232f415c522851dede979f5d609dda844fd3))
* 缺失运行时嵌套依赖时添加清除缓存提示 ([2d0b85b](https://github.com/certd/certd/commit/2d0b85b3054fc1aa82b3ab5db34a2a56308f71f2))
* 支持google persist校验方式 ([97d2b50](https://github.com/certd/certd/commit/97d2b5007409ec79ea864d437c473a2f9b5dfae4))
* **config:** 修复环境变量加载逻辑并更新配置文件 ([07f0d3b](https://github.com/certd/certd/commit/07f0d3b4d626628a0e3dfc7e5c52e8c4a98bad47))
* **ctyun-cdn:** 修复天翼云CDN部署插件的域名选择问题 ([ea7d14c](https://github.com/certd/certd/commit/ea7d14c289b8720781bf101bfef59eac7bb03765))
* **webhook:** 修复模版转义换行符,避免手动上传的证书报错 ([2735feb](https://github.com/certd/certd/commit/2735feb3302f8308708dafc8b295adcd4b91d7f0))
### Features
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
### Performance Improvements
* 宝塔站点列表支持搜索和排序 ([4420472](https://github.com/certd/certd/commit/4420472f8fac272d30fb8ccb1cc2ee9763989001))
* 网宿插件双鉴权支持 ([241b2e5](https://github.com/certd/certd/commit/241b2e55848471e61b5d2be0c87a7dc9a0d29557))
* **monitor:** 站点证书监控支持手动选中触发检查 ([f60e63f](https://github.com/certd/certd/commit/f60e63fbaf00460d5050cd7705bd75d57dec202d))
* **open-api:** openkey增加用途备注 ([bbeaf61](https://github.com/certd/certd/commit/bbeaf617d19750515f6eca51863e74e23bc1de50))
* **tencent clb:** 修改所有CLB地域的endpoint为就近接入 ([c427735](https://github.com/certd/certd/commit/c4277350bcc8cf034cf4e76db2a6daa1842bc5d7))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Bug Fixes
* 修复添加步骤会修改所有步骤标题的bug ([51e83e4](https://github.com/certd/certd/commit/51e83e4c38f7f319314fdc38ef33cf5720b2dde0))
* 修复证书插件配置无法打开的bug ([5563abb](https://github.com/certd/certd/commit/5563abb13a62140f4dee668ab768cf4ff9371ddb))
### Performance Improvements
* 新增余额管理,管理员充值功能 ([bded368](https://github.com/certd/certd/commit/bded368cbb8974b6937ae260e448c25fae5c33ad))
* **acme-account-access:** AcmeAccount授权允许修改eab,重新生成account ([6a649aa](https://github.com/certd/certd/commit/6a649aaca083ccd3b517f7d676a58220aa33b37d))
* **ui:** 商业版套餐购买不要求设置支付方式 ([e971145](https://github.com/certd/certd/commit/e9711456860d7db40914a0c295534f7bca56f83c))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
### Bug Fixes
* **plugin-service:** 修复同步自己开发的云端插件不显示已安装状态问题 ([dac94b6](https://github.com/certd/certd/commit/dac94b64f0ceb7176e2cf07ce220be0aa5ed98d2))
* **plugin:** 修复插件配置保存与加载的相关问题 ([bd94334](https://github.com/certd/certd/commit/bd9433478a43d5d64ce6370386d0fe0aaff07d2e))
### Performance Improvements
* **plugin:** add Fastly custom TLS certificates integration ([#797](https://github.com/certd/certd/issues/797)) [@szhu25](https://github.com/szhu25) ([2a2e963](https://github.com/certd/certd/commit/2a2e963914c4dfc192cbe0a4bbb9c115e626da01))
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
### Bug Fixes
+10 -15
View File
@@ -105,14 +105,17 @@ https://certd.handfree.work/
#### Docker镜像说明:
##### 1. 镜像地址格式:
##### 1. 镜像地址:
```
registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型 (中括号表示可选)
| 镜像仓库 | 最新预览版 | 稳定版 | slim版 | armv7版 |
| --- | --- | --- | --- | --- |
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
| Docker Hub | `certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
```
##### 2. 版本标签:
> 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
##### 2. 版本标签说明:
**最新版本标签:**
@@ -131,16 +134,8 @@ registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` | `certd:1.43.0-slim` |
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` | `certd:1.43.0-armv7` |
##### 3. 镜像地址:
| 镜像仓库 | 最新预览版 | slim | armv7 |
| --- | --- | --- | --- |
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` |
| Docker Hub | `certd/certd:latest` | `certd:slim` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` |
> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
> 2. 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
> 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
最新版本号:
![GitHub release (latest by date)](https://img.shields.io/github/v/release/certd/certd?label=Latest%20Release&sort=semver)
+31
View File
@@ -3,6 +3,37 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Bug Fixes
* 修复添加步骤会修改所有步骤标题的bug ([51e83e4](https://github.com/certd/certd/commit/51e83e4c38f7f319314fdc38ef33cf5720b2dde0))
* 修复证书插件配置无法打开的bug ([5563abb](https://github.com/certd/certd/commit/5563abb13a62140f4dee668ab768cf4ff9371ddb))
### Performance Improvements
* 新增余额管理,管理员充值功能 ([bded368](https://github.com/certd/certd/commit/bded368cbb8974b6937ae260e448c25fae5c33ad))
* **acme-account-access:** AcmeAccount授权允许修改eab,重新生成account ([6a649aa](https://github.com/certd/certd/commit/6a649aaca083ccd3b517f7d676a58220aa33b37d))
* **ui:** 商业版套餐购买不要求设置支付方式 ([e971145](https://github.com/certd/certd/commit/e9711456860d7db40914a0c295534f7bca56f83c))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
### Bug Fixes
* **plugin-service:** 修复同步自己开发的云端插件不显示已安装状态问题 ([dac94b6](https://github.com/certd/certd/commit/dac94b64f0ceb7176e2cf07ce220be0aa5ed98d2))
* **plugin:** 修复插件配置保存与加载的相关问题 ([bd94334](https://github.com/certd/certd/commit/bd9433478a43d5d64ce6370386d0fe0aaff07d2e))
### Performance Improvements
* **plugin:** add Fastly custom TLS certificates integration ([#797](https://github.com/certd/certd/issues/797)) [@szhu25](https://github.com/szhu25) ([2a2e963](https://github.com/certd/certd/commit/2a2e963914c4dfc192cbe0a4bbb9c115e626da01))
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
### Bug Fixes
* 修复插件市场插件安装无效的bug ([15d2bd8](https://github.com/certd/certd/commit/15d2bd8484a9ddd142bf4ef03d839c74405f9e6d))
* preserve Route53 TXT record values ([#791](https://github.com/certd/certd/issues/791)) ([f5430a8](https://github.com/certd/certd/commit/f5430a8925cf6cf4e16c75f2358fbd78437e6705))
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
### Bug Fixes
+14 -18
View File
@@ -6,13 +6,17 @@
Certd 提供多种 Docker 镜像版本,您可以根据需要选择:
#### 1. 镜像地址格式:
#### 1. 镜像地址:
```
registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型
```
#### 2. 版本标签:
| 镜像仓库 | 最新预览版 | 稳定版 | slim版 | armv7版 |
| --- | --- | --- | --- | --- |
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
| Docker Hub | `certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
> 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
#### 2. 版本标签说明:
**最新版本标签:**
@@ -25,22 +29,14 @@ registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
> 根据基础镜像不同,分为如下三个分支版本,没有特殊需求选择默认的即可(他们功能是一样的)
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 | 指定版本 |
| --- | --- | --- | --- | --- | --- |
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小,支持x86、ARM架构 | `certd:stable` | `certd:1.43.0` |
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 | 指定版本 |
| --- | --- | --- | --- | --- | --- |
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小 | `certd:stable` | `certd:1.43.0` |
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` | `certd:1.43.0-slim` |
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` | `certd:1.43.0-armv7` |
#### 3. 镜像地址:
| 镜像仓库 | 最新预览版 | slim版 | armv7版 |
| --- | --- | --- | --- |
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` |
| Docker Hub | `certd/certd:latest` | `certd:slim` | `certd:armv7` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` |
> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
> 2. 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
> 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
最新版本号:
![GitHub release (latest by date)](https://img.shields.io/github/v/release/certd/certd?label=Latest%20Release&sort=semver)
+47 -46
View File
@@ -36,52 +36,53 @@
| 32.| **Dokploy授权** | |
| 33.| **Dynadot授权** | |
| 34.| **farcdn授权** | |
| 35.| **FlexCDN授权** | |
| 36.| **Gcore** | Gcore |
| 37.| **Github授权** | |
| 38.| **godaddy授权** | |
| 39.| **HiPM DNSMgr** | HiPM DNSMgr API Token 授权 |
| 40.| **金山云授权** | |
| 41.| **FTP授权** | |
| 42.| **七牛OSS授权** | |
| 43.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 |
| 44.| **s3/minio授权** | S3/minio oss授权 |
| 45.| **namesilo授权** | |
| 46.| **Next Terminal 授权** | 用于访问 Next Terminal API 的授权配置 |
| 47.| **Nginx Proxy Manager 授权** | 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 |
| 48.| **1panel授权** | 账号和密码 |
| 49.| **支付宝** | |
| 50.| **白山云授权** | |
| 51.| **宝塔云WAF授权** | 用于连接和管理宝塔云WAF服务的授权配置 |
| 52.| **cdnfly授权** | |
| 53.| **k8s授权** | |
| 54.| **括彩云cdn授权** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
| 55.| **LeCDN授权** | |
| 56.| **lucky** | |
| 57.| **猫云授权** | |
| 58.| **plesk授权** | |
| 59.| **长亭雷池授权** | |
| 60.| **群晖登录授权** | |
| 61.| **uniCloud** | unicloud授权 |
| 62.| **微信支付** | |
| 63.| **易盾rcdn授权** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
| 64.| **易发云短信** | sms.yfyidc.cn/ |
| 65.| **易盾DCDN授权** | https://user.yiduncdn.com |
| 66.| **易支付** | |
| 67.| **proxmox** | |
| 68.| **Spaceship.com 授权** | Spaceship.com API 授权插件 |
| 69.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器授权 |
| 70.| **UCloud授权** | 优刻得授权 |
| 71.| **又拍云** | |
| 72.| **网宿授权** | |
| 73.| **西部数码授权** | |
| 74.| **我爱云授权** | 我爱云CDN |
| 75.| **新网授权(代理方式)** | |
| 76.| **新网授权** | |
| 77.| **新网互联授权** | 仅支持代理账号,ip需要加入白名单 |
| 78.| **Zenlayer授权** | Zenlayer授权 |
| 79.| **GoEdge授权** | |
| 80.| **雨云授权** | https://app.rainyun.com/ |
| 35.| **Fastly授权** | Fastly CDN / TLS Custom Certificates API 授权 |
| 36.| **FlexCDN授权** | |
| 37.| **Gcore** | Gcore |
| 38.| **Github授权** | |
| 39.| **godaddy授权** | |
| 40.| **HiPM DNSMgr** | HiPM DNSMgr API Token 授权 |
| 41.| **金山云授权** | |
| 42.| **FTP授权** | |
| 43.| **七牛OSS授权** | |
| 44.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 |
| 45.| **s3/minio授权** | S3/minio oss授权 |
| 46.| **namesilo授权** | |
| 47.| **Next Terminal 授权** | 用于访问 Next Terminal API 的授权配置 |
| 48.| **Nginx Proxy Manager 授权** | 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 |
| 49.| **1panel授权** | 账号和密码 |
| 50.| **支付宝** | |
| 51.| **白山云授权** | |
| 52.| **宝塔云WAF授权** | 用于连接和管理宝塔云WAF服务的授权配置 |
| 53.| **cdnfly授权** | |
| 54.| **k8s授权** | |
| 55.| **括彩云cdn授权** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
| 56.| **LeCDN授权** | |
| 57.| **lucky** | |
| 58.| **猫云授权** | |
| 59.| **plesk授权** | |
| 60.| **长亭雷池授权** | |
| 61.| **群晖登录授权** | |
| 62.| **uniCloud** | unicloud授权 |
| 63.| **微信支付** | |
| 64.| **易盾rcdn授权** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
| 65.| **易发云短信** | sms.yfyidc.cn/ |
| 66.| **易盾DCDN授权** | https://user.yiduncdn.com |
| 67.| **易支付** | |
| 68.| **proxmox** | |
| 69.| **Spaceship.com 授权** | Spaceship.com API 授权插件 |
| 70.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器授权 |
| 71.| **UCloud授权** | 优刻得授权 |
| 72.| **又拍云** | |
| 73.| **网宿授权** | |
| 74.| **西部数码授权** | |
| 75.| **我爱云授权** | 我爱云CDN |
| 76.| **新网授权(代理方式)** | |
| 77.| **新网授权** | |
| 78.| **新网互联授权** | 仅支持代理账号,ip需要加入白名单 |
| 79.| **Zenlayer授权** | Zenlayer授权 |
| 80.| **GoEdge授权** | |
| 81.| **雨云授权** | https://app.rainyun.com/ |
<style module>
table th:first-of-type {
+25 -21
View File
@@ -1,5 +1,5 @@
# 任务插件
共 `135` 款任务插件
共 `139` 款任务插件
## 1. 证书申请
| 序号 | 名称 | 说明 |
@@ -31,26 +31,30 @@
| 4.| **中国移动-部署证书到CDN** | 中国移动自动部署证书到CDN |
| 5.| **多吉云-部署到多吉云CDN** | |
| 6.| **farcdn-更新证书** | www.farcdn.net |
| 7.| **FlexCDN-更新证书** | |
| 8.| **Gcore-刷新Gcore证书** | 刷新现有的证书 |
| 9.| **Gcore-部署证书到Gcore** | 仅上传 并不会部署到cdn |
| 10.| **GoEdge-更新证书** | GoEdge |
| 11.| **金山云-更新CDN证书** | 金山云自动更新CDN证书 |
| 12.| **白山云-更新证书** | |
| 13.| **cdnfly-部署证书到cdnfly** | cdnfly |
| 14.| **天翼云-部署证书到CDN** | 部署证书到天翼云CDN和全站加速 |
| 15.| **括彩云-部署到括彩云CDN** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
| 16.| **LeCDN-更新证书V2** | 支持新版本LeCDN |
| 17.| **LeCDN-更新证书** | |
| 18.| **Maoyun-更新猫云CDN证书** | |
| 19.| **易盾-部署到易盾DCDN** | 主要是防御,http://user.yiduncdn.com/ |
| 20.| **易盾-部署到易盾RCDN** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
| 21.| **雨云-更新证书** | app.rainyun.com |
| 22.| **又拍云-部署证书到CDN/USS** | 支持又拍云CDN,又拍云云存储USS |
| 23.| **网宿-更新证书** | 网宿证书自动更新 |
| 24.| **西数-部署到虚拟主机** | 西部数码部署证书到虚拟主机 |
| 25.| **我爱云-部署证书到我爱云CDN** | 部署证书到我爱云CDN |
| 26.| **Zenlayer-刷新证书** | 刷新Zenlayer CDN证书 |
| 7.| **Fastly-部署TLS激活** | 部署 Fastly 证书 (创建 TLS Activation 绑定证书到域名) |
| 8.| **Fastly-清除缓存** | 清除 Fastly 指定 Service 的所有缓存 (Purge All) |
| 9.| **Fastly-更新证书** | 自动更新 Fastly CDN 上的已有自定义证书 |
| 10.| **Fastly-上传证书到Fastly** | 部署 / 上传 SSL/TLS 自定义证书到 Fastly CDN |
| 11.| **FlexCDN-更新证书** | |
| 12.| **Gcore-刷新Gcore证书** | 刷新现有的证书 |
| 13.| **Gcore-部署证书到Gcore** | 仅上传 并不会部署到cdn |
| 14.| **GoEdge-更新证书** | GoEdge |
| 15.| **金山云-更新CDN证书** | 金山云自动更新CDN证书 |
| 16.| **白山云-更新证书** | |
| 17.| **cdnfly-部署证书到cdnfly** | cdnfly |
| 18.| **天翼云-部署证书到CDN** | 部署证书到天翼云CDN和全站加速 |
| 19.| **括彩云-部署到括彩云CDN** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
| 20.| **LeCDN-更新证书V2** | 支持新版本LeCDN |
| 21.| **LeCDN-更新证书** | |
| 22.| **Maoyun-更新猫云CDN证书** | |
| 23.| **易盾-部署到易盾DCDN** | 主要是防御,http://user.yiduncdn.com/ |
| 24.| **易盾-部署到易盾RCDN** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
| 25.| **雨云-更新证书** | app.rainyun.com |
| 26.| **又拍云-部署证书到CDN/USS** | 支持又拍云CDN,又拍云云存储USS |
| 27.| **网宿-更新证书** | 网宿证书自动更新 |
| 28.| **西数-部署到虚拟主机** | 西部数码部署证书到虚拟主机 |
| 29.| **我爱云-部署证书到我爱云CDN** | 部署证书到我爱云CDN |
| 30.| **Zenlayer-刷新证书** | 刷新Zenlayer CDN证书 |
## 4. 面板
| 序号 | 名称 | 说明 |
+2 -2
View File
@@ -5,8 +5,8 @@
`Certd`默认启用https,监听7002端口
如果你想关闭https,或者修改端口,可以在环境变量中配置
```shell
CERTD_HTTPS_ENABLE=true
CERTD_HTTPS_port=7002
certd_https_enabled=true
certd_https_port=7002
```
+1 -1
View File
@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.44.2"
"version": "1.45.0"
}
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/publishlab/node-acme-client/compare/v1.44.4...v1.45.0) (2026-10-02)
### Bug Fixes
* 支持google persist校验方式 ([97d2b50](https://github.com/publishlab/node-acme-client/commit/97d2b5007409ec79ea864d437c473a2f9b5dfae4))
## [1.44.4](https://github.com/publishlab/node-acme-client/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/acme-client
## [1.44.3](https://github.com/publishlab/node-acme-client/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/acme-client
## [1.44.2](https://github.com/publishlab/node-acme-client/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/acme-client
+4 -3
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.44.2",
"version": "1.45.0",
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.44.2",
"@certd/basic": "^1.45.0",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -27,6 +27,7 @@
"https-proxy-agent": "^7.0.5",
"lodash-es": "^4.17.21",
"node-forge": "^1.3.1",
"psl": "^1.15.0",
"punycode.js": "^2.3.1"
},
"devDependencies": {
@@ -76,5 +77,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
}
@@ -0,0 +1,21 @@
// @ts-ignore
import psl from "psl";
export function getDnsPersistIssuer(directoryUrl?: string) {
if (!directoryUrl) {
return "letsencrypt.org";
}
let directory: URL;
try {
directory = new URL(directoryUrl);
} catch {
throw new Error(`ACME Directory URL无效,无法生成DNS持久验证Issuer:${directoryUrl}`);
}
const issuer = psl.get(directory.hostname);
if (!issuer) {
throw new Error(`无法从ACME Directory URL获取可注册主域名:${directory.hostname}`);
}
return issuer;
}
+1
View File
@@ -79,3 +79,4 @@ export * from './verify.js'
export * from './error.js'
export * from './util.js'
export * from './dns-persist.js'
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Bug Fixes
* **webhook:** 修复模版转义换行符,避免手动上传的证书报错 ([2735feb](https://github.com/certd/certd/commit/2735feb3302f8308708dafc8b295adcd4b91d7f0))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/basic
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/basic
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/basic
+1 -1
View File
@@ -1 +1 @@
21:57
00:51
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.44.2",
"version": "1.45.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -54,5 +54,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
}
@@ -1,6 +1,21 @@
import dayjs from "dayjs";
export const stringUtils = {
replaceTemplate(target: string, body: Record<string, unknown>, urlEncode = false) {
let bodyStr = target;
for (const key of Object.keys(body)) {
const rawValue = body[key] == null ? "" : String(body[key]);
let value = urlEncode ? encodeURIComponent(rawValue) : rawValue;
value = value.replace(/[\u0000-\u001f]/g, char => {
const escapes: Record<string, string> = { "\b": "\\b", "\f": "\\f", "\n": "\\n", "\r": "\\r", "\t": "\\t" };
return escapes[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
});
bodyStr = bodyStr.replaceAll(`\${${key}}`, value);
bodyStr = bodyStr.replaceAll(`{${key}}`, value);
}
return bodyStr;
},
maxLength(str?: string, length = 100) {
if (str) {
return str.length > length ? str.slice(0, length) + "..." : str;
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Bug Fixes
* 缺失运行时嵌套依赖时添加清除缓存提示 ([2d0b85b](https://github.com/certd/certd/commit/2d0b85b3054fc1aa82b3ab5db34a2a56308f71f2))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/pipeline
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/pipeline
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/pipeline
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.44.2",
"version": "1.45.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -21,8 +21,8 @@
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.44.2",
"@certd/plus-core": "^1.44.2",
"@certd/basic": "^1.45.0",
"@certd/plus-core": "^1.45.0",
"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": "fe738fa7d4622016ed7d506c501df85393b6c857"
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
}
@@ -64,6 +64,28 @@ describe("RuntimeDepsService", () => {
assert.equal(mod.value, 42);
});
it("logs a cache clearing hint when a nested runtime dependency is missing", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-missing-nested-"));
const packageDir = path.join(rootDir, "node_modules", "runtime-only");
fs.mkdirSync(packageDir, { recursive: true });
fs.writeFileSync(path.join(rootDir, "package.json"), JSON.stringify({ name: "runtime-root", type: "module" }), "utf8");
fs.writeFileSync(path.join(packageDir, "package.json"), JSON.stringify({ name: "runtime-only", main: "index.js" }), "utf8");
fs.writeFileSync(path.join(packageDir, "index.js"), "module.exports = require('missing-nested');\n", "utf8");
const logs: string[] = [];
const logger = {
info() {},
warn() {},
error(message: string, ...args: any[]) {
logs.push([message, ...args.map(item => item?.message || String(item))].join(" "));
},
};
const service = new RuntimeDepsService({ rootDir }, null);
await assert.rejects(() => service.importRuntime("runtime-only", logger));
assert.ok(logs.some(message => message.includes("Cannot find module 'missing-nested'")));
assert.ok(logs.some(message => message.includes("插件管理页面->清除缓存依赖->重启certd")));
});
it("installs configured lazy dependency when import target is missing", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-"));
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "lazy-pkg": "^1.2.3" } }, null);
+11 -1
View File
@@ -251,6 +251,7 @@ export class RuntimeDepsService {
}
let resolved: string;
let fromRuntimeDeps = false;
try {
//尝试加载本项目依赖
resolved = this.resolveProjectSpecifier(specifier).resolved;
@@ -258,13 +259,22 @@ export class RuntimeDepsService {
if (!this.isModuleNotFoundError(error)) {
throw error;
}
fromRuntimeDeps = true;
resolved = await this.resolveLazyOrInstallSpecifier(specifier, logger);
}
if (!resolved) {
throw new Error(`依赖未安装成功: ${specifier}`);
}
return await import(pathToFileURL(resolved).href);
try {
return await import(pathToFileURL(resolved).href);
} catch (error: any) {
if (fromRuntimeDeps && this.isModuleNotFoundError(error)) {
logger.error?.("动态依赖加载失败:", error);
logger.error?.("动态依赖加载失败,请前往插件管理页面->清除缓存依赖->重启certd后重试");
}
throw error;
}
}
//动态获取模块解析
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/lib-huawei
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/lib-huawei
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/lib-huawei
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/lib-huawei
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.44.2",
"version": "1.45.0",
"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": "fe738fa7d4622016ed7d506c501df85393b6c857"
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
}
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/lib-iframe
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/lib-iframe
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/lib-iframe
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/lib-iframe
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.44.2",
"version": "1.45.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -37,5 +37,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
}
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/jdcloud
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/jdcloud
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/jdcloud
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/jdcloud
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.44.2",
"version": "1.45.0",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -63,5 +63,5 @@
"fetch"
]
},
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
}
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/lib-k8s
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/lib-k8s
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/lib-k8s
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/lib-k8s
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.44.2",
"version": "1.45.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -21,7 +21,7 @@
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.44.2",
"@certd/basic": "^1.45.0",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
}
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Features
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Bug Fixes
* 修复证书插件配置无法打开的bug ([5563abb](https://github.com/certd/certd/commit/5563abb13a62140f4dee668ab768cf4ff9371ddb))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/lib-server
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/lib-server
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.44.2",
"version": "1.45.0",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -29,11 +29,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.44.2",
"@certd/basic": "^1.44.2",
"@certd/pipeline": "^1.44.2",
"@certd/plugin-lib": "^1.44.2",
"@certd/plus-core": "^1.44.2",
"@certd/acme-client": "^1.45.0",
"@certd/basic": "^1.45.0",
"@certd/pipeline": "^1.45.0",
"@certd/plugin-lib": "^1.45.0",
"@certd/plus-core": "^1.45.0",
"@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": "fe738fa7d4622016ed7d506c501df85393b6c857"
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
}
@@ -133,12 +133,12 @@ export class SysPrivateSettings extends BaseSettings {
// 内置项不在此配置 Directory URL:其端点由运行时按加密算法(pkType)通过 acme.getDirectoryUrl 获取,
// 例如 ZeroSSL、SSL.com 的 RSA 与 EC 证书使用不同的端点,写死单一 URL 会导致 EC 申请走错端点。
customAcmeProviders?: CustomAcmeProvider[] = [
{ sslProvider: "letsencrypt", title: "Let's Encrypt", directoryUrl: "", needEAB: false, builtIn: true },
{ sslProvider: "letsencrypt_staging", title: "Let's Encrypt测试环境", directoryUrl: "", needEAB: false, builtIn: true },
{ sslProvider: "letsencrypt", title: "Let's Encrypt(支持7天IP证书)", directoryUrl: "", needEAB: false, builtIn: true },
{ sslProvider: "google", title: "Google", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "zerossl", title: "ZeroSSL", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "litessl", title: "litessl", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "litessl", title: "Litessl", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "sslcom", title: "SSL.com", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "letsencrypt_staging", title: "Let's Encrypt测试环境(只能生成测试证书)", directoryUrl: "", needEAB: false, builtIn: true },
];
removeSecret() {
@@ -174,11 +174,11 @@ export class AccessService extends BaseService<AccessEntity> {
throw new ValidateException("userId不能为空");
}
if (userId !== entity.userId) {
throw new PermissionException("您对该Access授权无访问权限");
throw new PermissionException(`您对该Access授权${entity.name}(${entity.id})无访问权限`);
}
}
if (projectId != null && projectId !== entity.projectId) {
throw new PermissionException("您对该Access授权无访问权限");
throw new PermissionException(`您对该Access授权${entity.name}(${entity.id})无访问权限`);
}
// const access = accessRegistry.get(entity.type);
@@ -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.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/midway-flyway-js
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.44.2",
"version": "1.45.0",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -52,5 +52,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
}
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/plugin-cert
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/plugin-cert
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/plugin-cert
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/plugin-cert
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.44.2",
"version": "1.45.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -20,7 +20,7 @@
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/plugin-lib": "^1.44.2"
"@certd/plugin-lib": "^1.45.0"
},
"devDependencies": {
"@types/chai": "^4.3.12",
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
}
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/plugin-lib
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/plugin-lib
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/plugin-lib
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
**Note:** Version bump only for package @certd/plugin-lib
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.44.2",
"version": "1.45.0",
"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.44.2",
"@certd/basic": "^1.44.2",
"@certd/pipeline": "^1.44.2",
"@certd/acme-client": "^1.45.0",
"@certd/basic": "^1.45.0",
"@certd/pipeline": "^1.45.0",
"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": "fe738fa7d4622016ed7d506c501df85393b6c857"
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
}
+29
View File
@@ -3,6 +3,35 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Features
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
### Performance Improvements
* **monitor:** 站点证书监控支持手动选中触发检查 ([f60e63f](https://github.com/certd/certd/commit/f60e63fbaf00460d5050cd7705bd75d57dec202d))
* **open-api:** openkey增加用途备注 ([bbeaf61](https://github.com/certd/certd/commit/bbeaf617d19750515f6eca51863e74e23bc1de50))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Bug Fixes
* 修复添加步骤会修改所有步骤标题的bug ([51e83e4](https://github.com/certd/certd/commit/51e83e4c38f7f319314fdc38ef33cf5720b2dde0))
* 修复证书插件配置无法打开的bug ([5563abb](https://github.com/certd/certd/commit/5563abb13a62140f4dee668ab768cf4ff9371ddb))
### Performance Improvements
* 新增余额管理,管理员充值功能 ([bded368](https://github.com/certd/certd/commit/bded368cbb8974b6937ae260e448c25fae5c33ad))
* **ui:** 商业版套餐购买不要求设置支付方式 ([e971145](https://github.com/certd/certd/commit/e9711456860d7db40914a0c295534f7bca56f83c))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
### Bug Fixes
* **plugin:** 修复插件配置保存与加载的相关问题 ([bd94334](https://github.com/certd/certd/commit/bd9433478a43d5d64ce6370386d0fe0aaff07d2e))
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.44.2",
"version": "1.45.0",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -104,8 +104,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.44.2",
"@certd/pipeline": "^1.44.2",
"@certd/lib-iframe": "^1.45.0",
"@certd/pipeline": "^1.45.0",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -19,6 +19,7 @@ import sysauthority from "./certd/sys-authority";
import syscname from "./certd/sys-cname";
import tutorial from "./certd/tutorial";
import cron from "./certd/cron";
import client from "./certd/client";
// Note: @ is reserved in locale messages; use {'@'} when needed.
export default {
@@ -43,4 +44,5 @@ export default {
...syscname,
...tutorial,
...cron,
...client,
};
@@ -19,6 +19,7 @@ export default {
subdomainConfirmContent: "{domain} appears to be a subdomain. Only delegated subdomains and free second-level subdomains need to be maintained here. Otherwise certificate application may fail. Continue?",
importFromProvider: "Import from Domain Provider",
importFromResolveRecords: "Import from DNS Records",
importFromResolveRecordsTooltip: "Import DNS records as monitoring sites",
syncExpirationDate: "Sync Domain Expiration Time",
syncTaskSubmitted: "Sync task submitted",
syncExpirationProgress: "Sync Domain Expiration Progress",
@@ -0,0 +1,20 @@
export default {
clientManager: "Client Manager",
client: {
title: "Client Manager",
description: "View online status and managed site count of connected Certd clients. Clients report a heartbeat every 10 minutes and are marked offline if no heartbeat arrives for 30 minutes.",
plusRequired: "This feature requires the Pro edition",
machineName: "Machine",
clientId: "Client ID",
version: "Version",
os: "OS",
status: "Status",
online: "Online",
offline: "Offline",
siteCount: "Sites",
httpsSiteCount: "HTTPS Sites",
syncedSiteCount: "Synced",
failedSiteCount: "Failed",
lastHeartbeatAt: "Last Heartbeat",
},
};
@@ -53,6 +53,8 @@ export default {
suiteSetting: "Suite Settings",
orderManager: "Order Management",
userSuites: "User Suites",
balanceManager: "Balance Management",
balanceLog: "Balance Change Records",
inviteCommissionSetting: "Incentive Plan Settings",
inviteLevel: "Promotion Levels",
inviteUserLevel: "User Promotion Levels",
@@ -3,6 +3,9 @@ export default {
scopeOpenApiOnly: "Open API Only",
scopeFullAccount: "Full Account Permissions",
scopeHelper: "Open API only allows access to open APIs; full account permissions allow access to all APIs",
keyRemark: "Key Purpose",
keyRemarkHelper: "Describe the purpose of this key for easier identification",
keyRemarkMaxChars: "Maximum 512 characters",
gen: {
text: "API Test",
title: "x-certd-token",
@@ -7,6 +7,8 @@ export default {
settingLink: "Site Monitoring Settings",
limitInfo: "Basic edition limited to 1, professional and above unlimited, current",
checkAll: "Check All",
checkBatch: "Check Selected",
batchCheckConfirm: "Check the {count} selected sites now?",
checkNow: "Check Now",
batchDelete: "Batch Delete",
confirmTitle: "Confirm",
@@ -19,6 +19,7 @@ import sysauthority from "./certd/sys-authority";
import syscname from "./certd/sys-cname";
import tutorial from "./certd/tutorial";
import cron from "./certd/cron";
import client from "./certd/client";
// Note: @ is reserved in locale messages; use {'@'} when needed.
export default {
@@ -43,4 +44,5 @@ export default {
...syscname,
...tutorial,
...cron,
...client,
};
@@ -19,6 +19,7 @@ export default {
subdomainConfirmContent: "检测到{domain}为子域名,只有托管子域名和免费二级子域名才需要在此处维护,否则会导致申请证书失败,请确认是否继续?",
importFromProvider: "从域名提供商导入",
importFromResolveRecords: "从解析记录导入",
importFromResolveRecordsTooltip: "从域名的解析记录中导入并作为监控站点",
syncExpirationDate: "同步域名过期时间",
syncTaskSubmitted: "同步任务已提交",
syncExpirationProgress: "同步域名过期时间进度",
@@ -0,0 +1,20 @@
export default {
clientManager: "客户端管理",
client: {
title: "客户端管理",
description: "查看已接入的 Certd 客户端在线状态与管理的站点数量;客户端每 10 分钟上报一次心跳,超过 30 分钟未上报则标记为离线",
plusRequired: "此功能需要专业版",
machineName: "机器名",
clientId: "客户端ID",
version: "版本",
os: "系统",
status: "在线状态",
online: "在线",
offline: "离线",
siteCount: "站点数",
httpsSiteCount: "HTTPS站点",
syncedSiteCount: "已同步",
failedSiteCount: "异常",
lastHeartbeatAt: "上次心跳",
},
};
@@ -54,6 +54,8 @@ export default {
suiteSetting: "套餐设置",
orderManager: "订单管理",
userSuites: "用户套餐",
balanceManager: "余额管理",
balanceLog: "余额变更记录",
inviteCommissionSetting: "激励计划设置",
inviteLevel: "推广等级",
inviteUserLevel: "用户推广等级",
@@ -3,6 +3,9 @@ export default {
scopeOpenApiOnly: "仅开放接口",
scopeFullAccount: "账户所有权限",
scopeHelper: "仅开放接口只可以访问开放接口,账户所有权限可以访问所有接口",
keyRemark: "密钥用途",
keyRemarkHelper: "说明该密钥的用途,便于区分和查找",
keyRemarkMaxChars: "最大512个字符",
gen: {
text: "接口测试",
title: "x-certd-token",
@@ -7,6 +7,8 @@ export default {
settingLink: "站点监控设置",
limitInfo: "基础版限制1条,专业版以上无限制,当前",
checkAll: "检查全部",
checkBatch: "检查选中",
batchCheckConfirm: "确认触发检查选中的{count}个站点吗?",
checkNow: "立即检查",
batchDelete: "批量删除",
confirmTitle: "确认",
@@ -1,4 +1,4 @@
import { useProjectStore } from "/@/store/project";
import { useProjectStore } from "/@/store/project";
import { useSettingStore } from "/@/store/settings";
export const certdResources = [
@@ -128,6 +128,17 @@ export const certdResources = [
keepAlive: true,
},
},
{
title: "certd.clientManager",
name: "ClientManager",
path: "/cert/client",
component: "/certd/client/index.vue",
meta: {
icon: "ion:desktop-outline",
auth: true,
keepAlive: true,
},
},
{
title: "certd.sysResources.currentProject",
name: "ProjectMemberManager",
@@ -317,6 +317,34 @@ export const sysResources = [
keepAlive: true,
},
},
{
title: "certd.sysResources.balanceManager",
name: "SysWallet",
path: "/sys/suite/wallet",
component: "/sys/suite/wallet/index.vue",
meta: {
show: () => {
const settingStore = useSettingStore();
return settingStore.isComm;
},
icon: "ion:wallet-outline",
permission: "sys:settings:edit",
auth: true,
keepAlive: true,
},
},
{
title: "certd.sysResources.balanceLog",
name: "SysWalletLog",
path: "/sys/suite/wallet-log",
component: "/sys/suite/wallet/log/index.vue",
meta: {
isMenu: false,
permission: "sys:settings:view",
auth: true,
keepAlive: false,
},
},
{
title: "certd.sysResources.inviteCommissionSetting",
name: "SysInviteCommissionSetting",
@@ -26,6 +26,14 @@ export function fillPluginDefine(plugin: any) {
if (plugin.sysSetting?.metadata) {
merge(plugin.input, plugin.sysSetting.metadata?.input || {});
const keys = Object.keys(plugin.input);
for (const key of keys) {
//修复旧版本错误引入的key导致报错的问题 【多引入进了一个type=CertApply】
const column = plugin.input[key];
if (typeof column != "object") {
delete plugin.input[key];
}
}
// 应用选项映射
for (const key of Object.keys(plugin.input)) {
const inputDef = plugin.input[key];
@@ -51,10 +51,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
rowHandle: {
width: 120,
fixed: "right",
show: false,
buttons: {
view: { show: false },
edit: { show: false },
remove: { show: true },
remove: { show: false },
copy: { show: false },
},
},
@@ -3,7 +3,7 @@
<template #header>
<div class="title">
DNS持久验证记录
<span class="red sub" style="color: red">当前仅 Let's Encrypt 测试环境 可以申请 DNS 持久验证证书。</span>
<span class="red sub" style="color: red">当前仅 Google证书 可以申请 DNS 持久验证。</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
@@ -235,7 +235,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
} else if (value === "dns-persist") {
Modal.confirm({
title: "请前往DNS持久验证记录页面添加记录",
content: "DNS持久验证需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Let's Encrypt 测试环境 可以申请。",
content: "DNS持久验证需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Google证书 可以申请。",
async onOk() {
router.push({
path: "/cert/cert/dns-persist",
@@ -0,0 +1,29 @@
import { request } from "/src/api/service";
const apiPrefix = "/client";
export const clientApi = {
async GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
},
async GetObj(id: number) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id },
});
},
async DelObj(id: number) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
},
};
@@ -0,0 +1,243 @@
// @ts-ignore
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import dayjs from "dayjs";
import { useDicts } from "../dicts";
import { useProjectStore } from "/@/store/project";
import { useI18n } from "/src/locales";
import { clientApi } from "./api";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const api = clientApi;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const delRequest = async (req: DelReq) => {
const { row } = req;
return await api.DelObj(row.id);
};
const editRequest = async (req: EditReq) => {};
const addRequest = async (req: AddReq) => {};
const { myProjectDict } = useDicts();
const projectStore = useProjectStore();
return {
id: "clientCrud",
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
pagination: {
pageSizeOptions: ["10", "20", "50", "100", "200"],
},
actionbar: {
buttons: {
add: {
show: false,
},
},
},
rowHandle: {
fixed: "right",
width: 120,
buttons: {
view: {
show: false,
},
edit: {
show: false,
},
},
},
search: {
initialForm: {
...projectStore.getSearchForm(),
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
search: {
show: false,
},
column: {
width: 70,
align: "center",
},
form: {
show: false,
},
},
machineName: {
title: t("certd.client.machineName"),
search: {
show: true,
},
type: "text",
form: {
show: false,
},
column: {
width: 180,
ellipsis: true,
},
},
clientId: {
title: t("certd.client.clientId"),
search: {
show: true,
},
type: "text",
form: {
show: false,
},
column: {
width: 220,
ellipsis: true,
},
},
version: {
title: t("certd.client.version"),
search: {
show: false,
},
type: "text",
form: {
show: false,
},
column: {
width: 90,
align: "center",
},
},
os: {
title: t("certd.client.os"),
search: {
show: false,
},
type: "text",
form: {
show: false,
},
column: {
width: 100,
align: "center",
},
},
online: {
title: t("certd.client.status"),
search: {
show: false,
},
type: "text",
form: {
show: false,
},
column: {
width: 100,
align: "center",
cellRender({ value, row }) {
const online = value === true;
const heartbeat = row.lastHeartbeatAt ? dayjs(row.lastHeartbeatAt).format("YYYY-MM-DD HH:mm:ss") : "";
return (
<a-tooltip title={heartbeat}>
<a-tag color={online ? "green" : "red"}>{online ? t("certd.client.online") : t("certd.client.offline")}</a-tag>
</a-tooltip>
);
},
},
},
siteCount: {
title: t("certd.client.siteCount"),
search: {
show: false,
},
type: "number",
form: {
show: false,
},
column: {
width: 90,
align: "center",
},
},
httpsSiteCount: {
title: t("certd.client.httpsSiteCount"),
search: {
show: false,
},
type: "number",
form: {
show: false,
},
column: {
width: 110,
align: "center",
},
},
syncedSiteCount: {
title: t("certd.client.syncedSiteCount"),
search: {
show: false,
},
type: "number",
form: {
show: false,
},
column: {
width: 90,
align: "center",
},
},
failedSiteCount: {
title: t("certd.client.failedSiteCount"),
search: {
show: false,
},
type: "number",
form: {
show: false,
},
column: {
width: 80,
align: "center",
},
},
lastHeartbeatAt: {
title: t("certd.client.lastHeartbeatAt"),
search: {
show: false,
},
type: "text",
form: {
show: false,
},
column: {
width: 170,
sorter: true,
cellRender({ value }) {
if (!value) {
return "-";
}
return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
},
},
},
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
dict: myProjectDict,
form: {
show: false,
},
},
},
},
};
}
@@ -0,0 +1,37 @@
<template>
<fs-page>
<template #header>
<div class="title flex items-center">
{{ t("certd.client.title") }}
<div class="sub flex items-center flex-1">
<div>
{{ t("certd.client.description") }}
</div>
<vip-button class="ml-5" mode="button"></vip-button>
</div>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
import { useSettingStore } from "/@/store/settings";
import { useI18n } from "/src/locales";
const { t } = useI18n();
const settingStore = useSettingStore();
defineOptions({
name: "ClientManager",
});
const context: any = {
permission: {
isProjectPermission: true,
},
};
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
useMounted(() => crudExpose.doRefresh());
</script>
@@ -63,6 +63,13 @@ export const siteInfoApi = {
method: "post",
});
},
async CheckBatch(ids: number[]) {
return await request({
url: apiPrefix + "/checkBatch",
method: "post",
data: { ids },
});
},
async Import(form: any) {
return await request({
@@ -100,6 +100,23 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
}
function checkBatch() {
if (selectedRowKeys.value.length === 0) {
notification.error({ message: t("monitor.selectRecordsFirst") });
return;
}
Modal.confirm({
title: t("monitor.confirmTitle"),
content: t("monitor.batchCheckConfirm", { count: selectedRowKeys.value.length }),
onOk: async () => {
await siteInfoApi.CheckBatch(selectedRowKeys.value);
notification.success({ message: t("monitor.checkSubmitted") });
selectedRowKeys.value = [];
await crudExpose.doRefresh();
},
});
}
const GroupTypeSite = "site";
const groupDictRef = createGroupDictRef(GroupTypeSite);
@@ -253,6 +270,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: hasActionPermission("write"),
title: t("certd.domain.importFromResolveRecords"),
text: t("certd.domain.importFromResolveRecords"),
tooltip: { title: t("certd.domain.importFromResolveRecordsTooltip") },
type: "primary",
// needPlus: true,
color: "gold",
@@ -274,6 +292,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
checkAll();
},
},
checkBatch: {
show: true,
text: t("monitor.checkBatch"),
type: "primary",
icon: "ion:play-forward-outline",
click() {
checkBatch();
},
},
},
},
rowHandle: {
@@ -13,10 +13,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
// 编辑允许修改备注与权限范围。
// keyId/keySecret 属于密钥本体,列表中 keySecret 是脱敏串,误提交会把真实密钥写坏,故不提交。
const editRequest = async (req: EditReq) => {
const { form, row } = req;
form.id = row.id;
const res = await api.UpdateObj(form);
const res = await api.UpdateObj({
id: row.id,
remark: form.remark,
scope: form.scope,
});
return res;
};
const delRequest = async (req: DelReq) => {
@@ -68,7 +73,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
buttons: {
view: { show: true },
copy: { show: false },
edit: { show: false },
edit: { show: true },
remove: { show: true },
gen: {
text: t("certd.gen.text"),
@@ -193,6 +198,22 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
sorter: true,
},
},
remark: {
title: t("certd.keyRemark"),
type: "text",
search: {
show: true,
},
column: {
width: 200,
sorter: true,
},
form: {
show: true,
helper: t("certd.keyRemarkHelper"),
rules: [{ max: 512, message: t("certd.keyRemarkMaxChars") }],
},
},
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
@@ -280,7 +280,10 @@ function useStepForm() {
return;
}
callback.value("save", currentStep);
// currentStep 是复用同一份 reactive 对象,直接传给回调会被 task.steps 保存同一引用,
// 后续再次 stepOpen 会 delete 掉其属性,导致已保存的步骤(如第一个步骤)被覆盖。
// 这里深拷贝一份再传出,保证每个步骤是独立对象。
callback.value("save", cloneDeep(currentStep));
stepDrawerClose();
};
@@ -53,9 +53,14 @@
<div class="flex-o mt-5">
<span class="label">{{ $t("certd.order.paymentMethod") }}:</span>
<div v-if="durationSelected.price === 0 || expectedThirdPartyAmount === 0">{{ $t("certd.order.free") }}</div>
<div v-if="durationSelected.price === 0 || expectedThirdPartyAmount === 0">0元</div>
<div v-else-if="noPaymentMethod">当前仅支持余额支付</div>
<fs-dict-select v-else v-model:value="formRef.payType" :dict="paymentsDictRef" style="width: 200px"> </fs-dict-select>
</div>
<div v-if="showBalanceInsufficientTip" class="flex-o mt-5">
<span class="color-red">余额不足,请联系管理员,充值或设置套餐</span>
</div>
</div>
</a-modal>
</template>
@@ -88,12 +93,17 @@ async function open(opts: OrderModalOpenReq) {
formRef.value.productId = opts.product.id;
formRef.value.duration = opts.duration;
formRef.value.num = opts.num ?? 1;
formRef.value.useRebateBalance = false;
formRef.value.useRebateBalance = true;
try {
wallet.value = await GetWalletSummary();
} catch (e) {
wallet.value = { availableAmount: 0 };
}
try {
await paymentsDictRef.loadDict();
} catch (e) {
// 支付方式加载失败时按无可用支付方式处理
}
}
const paymentsDictRef = dict({
async getData() {
@@ -119,6 +129,23 @@ const expectedThirdPartyAmount = computed(() => {
return Math.max(0, (durationSelected.value?.price || 0) - expectedRebateAmount.value);
});
const noPaymentMethod = computed(() => {
return paymentsDictRef.data.length === 0;
});
const balanceInsufficient = computed(() => {
const price = durationSelected.value?.price || 0;
if (price <= 0) {
return false;
}
const availableAmount = wallet.value?.availableAmount || 0;
return availableAmount < price;
});
const showBalanceInsufficientTip = computed(() => {
return noPaymentMethod.value && balanceInsufficient.value;
});
function amountToYuan(amount: number) {
return util.amount.toYuan(amount || 0);
}
@@ -21,7 +21,17 @@
</div>
</template>
<template v-else-if="step === 1"
><label class="plugin-ai-dev__label">开发需求</label><a-textarea v-model:value="requirement" :rows="6" /><a-button
><div v-if="mode === 'new'" class="plugin-ai-dev__field">
<label>插件作者</label>
<PluginAuthorField v-model="author" editable />
<div class="plugin-ai-dev__hint">自动读取绑定账号已注册的插件作者,也可以手动输入作者名称。</div>
</div>
<div v-else class="plugin-ai-dev__field">
<label>插件作者</label>
<a-input :value="author" disabled />
<div class="plugin-ai-dev__hint">修改已有插件时沿用该插件当前作者,作者变化会导致插件身份变化。</div>
</div>
<label class="plugin-ai-dev__label">开发需求</label><a-textarea v-model:value="requirement" :rows="6" /><a-button
class="plugin-ai-dev__generate"
type="primary"
:loading="creating"
@@ -120,6 +130,7 @@ import * as api from "../api";
import * as pipelineApi from "/@/views/certd/pipeline/api";
import * as historyApi from "/@/views/certd/pipeline/api.history";
import DependPluginsInput from "./depend-plugins-input.vue";
import PluginAuthorField from "./plugin-author-field.vue";
import PluginEditDialogBody from "./plugin-edit-dialog-body.vue";
import PipelineEditDialog from "/@/views/certd/pipeline/components/pipeline-edit-dialog.vue";
import { useFormDialog } from "/@/use/use-dialog";
@@ -135,6 +146,7 @@ const pluginTypeOptions = [
{ value: "notification", label: "通知插件", desc: "通过消息、邮件等方式发送通知" },
// { value: "addon", label: "Addon", desc: "扩展流水线的通用辅助能力" },
];
const author = ref("");
const requirement = ref(
"例如:开发一个部署证书到阿里云DCDN的插件\nAPI接口请参考:https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-overview。\n要求: \n1、能够引用上传到阿里云CAS步骤输出的证书。\n2、支持在UI上选择账户下某区域下的DCDN的域名列表。\n3、支持一次性选择多个域名进行部署"
);
@@ -183,7 +195,33 @@ async function createPrompt() {
try {
const token = await api.GetScopedAccessToken(["sys/ai"]);
accessToken.value = token.token;
prompt.value = `你是 Certd 在线插件开发 Agent。\n\n开发模式:${mode.value === "new" ? "开发新插件" : "修改已有插件"}\n插件类型:${pluginType.value || "按已有插件类型"}\n插件 ID:${pluginPath.value.at(-1) || "无"}\n用户需求:\n${requirement.value.trim()}\n\n如果用户需求描述与他选择的插件类型有冲突,你需要跟用户确认是否选错插件类型。 \nCertd 地址:${window.location.origin}\n受限 AccessToken(6小时有效):${token.token}\n\n请先读取 .trae/skills/certd-online-plugin-dev/SKILL.md,按插件类型开发并提交版本。仅调用 /scoped/sys/ai/plugin/ 前缀接口,完成后报告提交结果,不自动发布。\n如果当前工作目录不是 Certd 项目,或缺少 certd-online-plugin-dev Skill,先拉取 Certd 仓库代码并切换到仓库内工作(git clone https://atomgit.com/certd/certd --depth 1 )`;
const authorName = author.value.trim();
let authorText = authorName;
if (!authorText && mode.value === "edit") {
// 修改已有插件时作者保持原样,不能凭空补一个作者出来
authorText = "无(保持原插件的 author 字段不变,不要新增作者)";
}
if (!authorText) {
authorText = "未指定(请先向用户确认作者名称,不要自行编造作者名)";
}
prompt.value = `
你是 Certd 在线插件开发 Agent。
开发模式:${mode.value === "new" ? "开发新插件" : "修改已有插件"}
插件类型:${pluginType.value || "按已有插件类型"}
插件 ID:${pluginPath.value.at(-1) || "无"}
插件作者:${authorText}
用户需求:
${requirement.value.trim()}
如果用户需求描述与他选择的插件类型有冲突,你需要跟用户确认是否选错插件类型。
Certd 地址:${window.location.origin}
受限 AccessToken(6小时有效):${token.token}
请先读取 .trae/skills/certd-online-plugin-dev/SKILL.md,按插件类型开发并提交版本。仅调用 /scoped/sys/ai/plugin/ 前缀接口,完成后报告提交结果,不自动发布。
如果当前工作目录不是 Certd 项目,或缺少 certd-online-plugin-dev Skill,先拉取 Certd 仓库代码并切换到仓库内工作(git clone https://atomgit.com/certd/certd --depth 1 )
本次开发的在线插件无需写单元测试。
`;
notification.success({ message: "启动提示词已生成" });
} finally {
creating.value = false;
@@ -259,24 +297,31 @@ async function loadRecentPlugins(force = false) {
async function loadSelectedPlugin() {
const selectedValue = pluginPath.value.at(-1);
if (!selectedValue) {
recentPlugins.value = [];
setSelectedPlugin(null);
return;
}
if (props.pluginId && selectedValue === String(props.pluginId)) {
const selected = await api.GetObj(props.pluginId);
recentPlugins.value = selected ? [selected] : [];
setSelectedPlugin(selected);
return;
}
let selected = await api.GetObj(selectedValue);
if (selected) {
recentPlugins.value = [selected];
return;
if (!selected) {
const fullName = selectedValue.includes(":") ? selectedValue.slice(selectedValue.indexOf(":") + 1) : selectedValue;
const result: any = await api.FindPlugins({ includeBuiltIn: true, includeStore: true, keyword: fullName });
const records: any[] = Array.isArray(result) ? result : result?.records || [];
selected = records.find(item => String(item.fullName || item.name) === fullName) || records[0];
}
setSelectedPlugin(selected);
}
function setSelectedPlugin(plugin: any) {
recentPlugins.value = plugin ? [plugin] : [];
const pluginAuthor = String(plugin?.author || "").trim();
if (pluginAuthor) {
// 修改已有插件时,作者默认沿用该插件当前作者,避免插件身份被改错
author.value = pluginAuthor;
}
const fullName = selectedValue.includes(":") ? selectedValue.slice(selectedValue.indexOf(":") + 1) : selectedValue;
const result: any = await api.FindPlugins({ includeBuiltIn: true, includeStore: true, keyword: fullName });
const records: any[] = Array.isArray(result) ? result : result?.records || [];
selected = records.find(item => String(item.fullName || item.name) === fullName) || records[0];
recentPlugins.value = selected ? [selected] : [];
}
function formatTime(value: any) {
@@ -361,9 +406,9 @@ watch(mode, value => {
<style lang="less">
.plugin-ai-dev {
display: flex;
height: 60vh;
height: 65vh;
flex-direction: column;
gap: 20px;
gap: 10px;
&__steps {
flex: none;
@@ -373,7 +418,7 @@ watch(mode, value => {
display: flex;
flex: 1;
flex-direction: column;
gap: 18px;
gap: 10px;
padding: 8px 20px;
overflow-y: auto;
}
@@ -1,6 +1,6 @@
<template>
<div class="plugin-author-field">
<a-input :value="modelValue" placeholder="请输入作者名称" :disabled="authorRegistered" @update:value="emit('update:modelValue', $event)" />
<a-input :value="modelValue" placeholder="请输入作者名称" :disabled="inputDisabled" @update:value="emit('update:modelValue', $event)" />
<a-button type="link" size="small" :loading="loading" @click="handleAuthorAction">
{{ authorRegistered ? "修改邮箱" : "注册作者" }}
</a-button>
@@ -14,12 +14,22 @@ import { usePluginPublish } from "../use-publish";
defineOptions({ name: "PluginAuthorField" });
const props = defineProps<{ modelValue?: string }>();
const props = withDefaults(
defineProps<{
modelValue?: string;
// 允许手动输入作者名称,默认使用后端已注册作者且不可修改
editable?: boolean;
}>(),
{
editable: false,
}
);
const emit = defineEmits<{ (event: "update:modelValue", value: string): void }>();
const loading = ref(false);
const { registerPluginAuthor, updatePluginAuthorEmail } = usePluginPublish();
const registeredAuthor = ref<api.OnlinePluginAuthorBean>();
const authorRegistered = computed(() => !!registeredAuthor.value?.id);
const inputDisabled = computed(() => authorRegistered.value && !props.editable);
const isLocalAuthor = computed(() => {
return (
String(props.modelValue || "")
@@ -113,7 +113,8 @@ const onFinishFailed = (errorInfo: any) => {
const { openConfigDialog } = usePluginConfig();
async function doPluginConfig() {
const certApplyInfo = await getPluginSetting("CertApply");
const certApplyInfo: any = await getPluginSetting("CertApply");
certApplyInfo.pluginType = "plugin";
await openConfigDialog({
row: certApplyInfo,
onSuccess: async () => {
@@ -45,7 +45,7 @@
<script setup lang="tsx">
import { dict, FsRender } from "@fast-crud/fast-crud";
import { cloneDeep, merge, unset } from "lodash-es";
import { computed, onMounted, reactive, ref } from "vue";
import { computed, onMounted, provide, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import Rollbackable from "./rollbackable.vue";
import { fillPluginDefine, usePluginStore } from "/@/store/plugin";
@@ -185,9 +185,11 @@ const optionsMappingKey = {
},
editRender(item: any) {
return () => {
const options = item.component?.options || [];
if (options.length === 0) {
return <span class="text-gray-400">该组件没有预设选项</span>;
const options = reactive(cloneDeep(item.component?.options || []));
const showAdd = options.length === 0;
function addOption(opt: any) {
options.push(opt);
}
const onLabelChange = (optValue: string, newLabel: string) => {
@@ -233,6 +235,13 @@ const optionsMappingKey = {
))}
</tbody>
</table>
{showAdd && (
<div class="mt-2">
<a-button type="primary" size={() => addOption({ value: "", label: "" })}>
添加选项
</a-button>
</div>
)}
<div class="helper mt-1">只需填写需要自定义的选项,留空则使用原始显示内容</div>
</div>
);
@@ -289,6 +298,16 @@ onMounted(async () => {
defineExpose({
getForm,
});
provide("getCurrentPluginDefine", () => {
return { value: props.plugin };
});
provide("get:scope", () => {
return getScope();
});
provide("get:plugin:type", () => {
return props.plugin.pluginType;
});
</script>
<style lang="less">
@@ -14,10 +14,13 @@ import { usePluginStore } from "/@/store/plugin";
import PluginAuthorField from "./components/plugin-author-field.vue";
import { usePluginAiDev } from "./use-ai-dev";
import { useOnlineInstall } from "./use-online-install";
import dayjs from "dayjs";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const lastSyncTime = context.lastSyncTime;
const syncLoading = context.syncLoading;
const syncOnlinePlugins = context.syncOnlinePlugins;
let lastType = "";
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
if (lastType && lastType != query?.query?.type) {
@@ -65,10 +68,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const settingStore = useSettingStore();
const pluginStore = usePluginStore();
const syncLoading = ref(false);
const upgradeAllLoading = ref(false);
const lastSyncTime = ref(0);
const autoSyncInterval = 30 * 24 * 60 * 60 * 1000;
const syncButtonTitle = computed(() => {
if (!lastSyncTime.value) {
return t("certd.onlinePluginNotSynced");
@@ -79,39 +80,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
function formatSyncTime(time: number) {
return new Date(time).toLocaleString();
}
function needAutoSync(time: number) {
if (!time) {
return true;
}
return Date.now() - time > autoSyncInterval;
return dayjs(time).format("YYYY-MM-DD HH:mm:ss");
}
function isEditablePlugin(row: any) {
return row.editable === true;
}
async function syncOnlinePlugins(options?: { showSuccess?: boolean }) {
if (syncLoading.value) {
return;
}
syncLoading.value = true;
try {
await api.OnlinePluginSync();
const setting = await api.OnlinePluginSetting();
lastSyncTime.value = setting.lastSyncTime || Date.now();
await pluginStore.reload();
crudExpose.doRefresh();
if (options?.showSuccess !== false) {
notification.success({ message: t("certd.onlinePluginSyncSuccess") });
}
} finally {
syncLoading.value = false;
}
}
async function upgradeAllOnlinePlugins() {
if (upgradeAllLoading.value) {
return;
@@ -157,18 +132,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
upgradeAllLoading.value = false;
}
}
async function loadOnlinePluginSetting() {
const setting = await api.OnlinePluginSetting();
lastSyncTime.value = setting.lastSyncTime || 0;
if (needAutoSync(lastSyncTime.value)) {
await syncOnlinePlugins({ showSuccess: false });
}
}
loadOnlinePluginSetting().catch(e => {
console.warn("load online plugin setting failed", e);
});
return {
crudOptions: {
settings: {
@@ -22,18 +22,42 @@ import PluginItemCard from "./components/plugin-item-card.vue";
import OnlinePluginDetail from "./components/online-plugin-detail.vue";
import { useI18n } from "/src/locales";
import { useMounted } from "/@/use/use-mounted";
import { computed, onBeforeUnmount, onMounted } from "vue";
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { usePluginStore } from "/@/store/plugin";
import { useFormDialog } from "/@/use/use-dialog";
import { notification } from "ant-design-vue";
import * as api from "./api";
const { t } = useI18n();
const pluginStore = usePluginStore();
const { openFormDialog } = useFormDialog();
const lastSyncTime = ref<number>(0);
const syncLoading = ref(false);
const autoSyncInterval = 3 * 24 * 60 * 60 * 1000;
defineOptions({
name: "SysPlugin",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
async function syncOnlinePlugins(options?: { showSuccess?: boolean }) {
if (syncLoading.value) {
return;
}
syncLoading.value = true;
try {
await api.OnlinePluginSync();
const setting = await api.OnlinePluginSetting();
lastSyncTime.value = setting.lastSyncTime || Date.now();
await pluginStore.clear();
crudExpose.doRefresh();
if (options?.showSuccess !== false) {
notification.success({ message: t("certd.onlinePluginSyncSuccess") });
}
} finally {
syncLoading.value = false;
}
}
const context = { lastSyncTime, syncLoading, syncOnlinePlugins };
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
const pluginList = computed(() => {
return crudBinding.value?.data || [];
});
@@ -77,9 +101,25 @@ async function handlePluginChanged(payload: { action: string }) {
crudExpose.doRefresh();
}
function needAutoSync(time: number) {
if (!time) {
return true;
}
return Date.now() - time > autoSyncInterval;
}
async function autoSyncOnlinePlugins() {
const setting = await api.OnlinePluginSetting();
lastSyncTime.value = setting.lastSyncTime || 0;
if (needAutoSync(lastSyncTime.value)) {
await syncOnlinePlugins({ showSuccess: false });
}
}
// 页面打开后获取列表数据
useMounted(async () => {
await crudExpose.doRefresh();
autoSyncOnlinePlugins();
crudExpose.doRefresh();
});
onMounted(() => {
@@ -49,7 +49,7 @@ export function usePluginConfig() {
}
}
const res = await api.savePluginSetting({
name: row.fullName,
name: row.fullName || row.name,
sysSetting: {
metadata: {
input: newForm,
@@ -3,7 +3,7 @@
<template #header>
<div class="title">
套餐设置
<span class="sub"> 需要<router-link :to="{ path: '/sys/settings', query: { tab: 'payment' } }">开启至少一种支付方式</router-link></span>
<span class="sub"> 建议<router-link :to="{ path: '/sys/settings', query: { tab: 'payment' } }">开启至少一种支付方式</router-link></span>
</div>
</template>
@@ -38,7 +38,7 @@
<a-form-item label=" " :colon="false">
<loading-button type="primary" html-type="button" :click="onClick">保存</loading-button>
<div class="helper">需要 <router-link :to="{ path: '/sys/settings', query: { tab: 'payment' } }">开启至少一种支付方式</router-link></div>
<div class="helper">建议 <router-link :to="{ path: '/sys/settings', query: { tab: 'payment' } }">开启至少一种支付方式</router-link></div>
</a-form-item>
</a-form>
</div>
@@ -0,0 +1,35 @@
import { request } from "/src/api/service";
const apiPrefix = "/sys/wallet";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
}
export async function Recharge(data: { userId: number; amount: number; remark?: string }) {
return await request({
url: apiPrefix + "/recharge",
method: "post",
data,
});
}
export async function GetWalletLogs(query: any) {
return await request({
url: apiPrefix + "/log/page",
method: "post",
data: query,
});
}
export async function GetSimpleUserByIds(ids: number[]) {
return await request({
url: "/sys/authority/user/getSimpleUserByIds",
method: "post",
data: { ids },
});
}
@@ -0,0 +1,193 @@
import { CreateCrudOptionsProps, CreateCrudOptionsRet, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { notification } from "ant-design-vue";
import { useRouter } from "vue-router";
import { util } from "/@/utils";
import { useFormDialog } from "/@/use/use-dialog";
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
import * as api from "./api";
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { openFormDialog } = useFormDialog();
const router = useRouter();
const userDict = dict({
async getNodesByValues(ids: number[]) {
return await api.GetSimpleUserByIds(ids);
},
value: "id",
label: "nickName",
});
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
function moneyColumn(title: string) {
return {
title,
type: "number",
column: {
width: 120,
component: {
name: PriceInput,
vModel: "modelValue",
edit: false,
},
},
};
}
async function openRecharge(userId?: number) {
await openFormDialog({
title: "用户余额充值",
wrapper: { width: 520 },
initialForm: {
userId: userId ?? null,
amountYuan: null,
remark: "",
},
columns: {
userId: {
title: "用户",
type: "table-select",
dict: userDict,
form: {
col: { span: 24 },
rules: [{ required: true, message: "请选择用户" }],
component: {
disabled: userId != null,
crossPage: true,
multiple: false,
select: {
placeholder: "请选择用户",
},
createCrudOptions: createCrudOptionsUser,
},
},
},
amountYuan: {
title: "充值金额",
type: "number",
form: {
col: { span: 24 },
rules: [{ required: true, message: "请输入充值金额" }],
component: {
name: "a-input-number",
vModel: "value",
min: 0,
precision: 2,
addonAfter: "元",
style: { width: "100%" },
},
},
},
remark: {
title: "备注",
type: "text",
form: {
col: { span: 24 },
helper: "选填",
},
},
},
async onSubmit(form: any) {
await api.Recharge({
userId: form.userId,
amount: util.amount.toCent(form.amountYuan || 0),
remark: form.remark,
});
await crudExpose.doRefresh();
notification.success({ message: "充值成功" });
},
});
}
return {
crudOptions: {
request: {
pageRequest,
},
toolbar: { show: false },
actionbar: {
buttons: {
add: { show: false },
recharge: {
text: "余额充值",
type: "primary",
click: () => openRecharge(),
},
log: {
text: "余额变更记录",
type: "primary",
click: () => router.push("/sys/suite/wallet-log"),
},
},
},
rowHandle: {
width: 220,
fixed: "right",
buttons: {
view: { show: false },
edit: { show: false },
copy: { show: false },
remove: { show: false },
walletLog: {
text: "余额变更记录",
type: "link",
click: ({ row }) => router.push({ path: "/sys/suite/wallet-log", query: { userId: row.userId } }),
},
recharge: {
text: "充值",
type: "link",
click: ({ row }) => openRecharge(row.userId),
},
},
},
columns: {
id: {
title: "ID",
type: "number",
column: { width: 80 },
form: { show: false },
},
userId: {
title: "用户",
type: "table-select",
dict: userDict,
search: { show: true, col: { span: 3 } },
column: { width: 160 },
form: {
show: false,
component: {
crossPage: true,
multiple: false,
select: {
placeholder: "点击选择用户",
},
createCrudOptions: createCrudOptionsUser,
},
},
},
availableAmount: moneyColumn("可用余额"),
totalAmount: moneyColumn("总余额"),
frozenAmount: moneyColumn("冻结余额"),
totalIncomeAmount: moneyColumn("累计收入"),
totalConsumedAmount: moneyColumn("累计消费"),
totalWithdrawAmount: moneyColumn("累计提现"),
createTime: {
title: "创建时间",
type: "datetime",
form: { show: false },
column: { sorter: true, width: 160 },
},
updateTime: {
title: "更新时间",
type: "datetime",
form: { show: false },
column: { width: 160 },
},
},
},
};
}
@@ -0,0 +1,28 @@
<template>
<fs-page>
<template #header>
<div class="title">
余额管理
<span class="sub">管理用户余额,可给用户充值</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
defineOptions({
name: "SysWallet",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
useMounted(async () => {
await crudExpose.doRefresh();
});
</script>
@@ -0,0 +1,100 @@
import { CreateCrudOptionsRet, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useRoute } from "vue-router";
import { util } from "/@/utils";
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
import * as api from "../api";
function moneyText(amount: number) {
const yuan = util.amount.toYuan(Math.abs(amount || 0));
if (amount < 0) {
return `-¥${yuan}`;
}
return `¥${yuan}`;
}
export default function (): CreateCrudOptionsRet {
const route = useRoute();
const userIdFromQuery = route.query.userId ? Number(route.query.userId) : undefined;
const userDict = dict({
async getNodesByValues(ids: number[]) {
return await api.GetSimpleUserByIds(ids);
},
value: "id",
label: "nickName",
});
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetWalletLogs(query);
};
return {
crudOptions: {
request: { pageRequest },
search: {
initialForm: { userId: userIdFromQuery },
},
actionbar: { show: false },
toolbar: { show: false },
rowHandle: { show: false },
columns: {
createTime: { title: "时间", type: "datetime", column: { width: 180 } },
userId: {
title: "用户",
type: "table-select",
dict: userDict,
search: { show: true, col: { span: 3 } },
column: { width: 160 },
form: {
show: false,
component: {
crossPage: true,
multiple: false,
select: { placeholder: "点击选择用户" },
createCrudOptions: createCrudOptionsUser,
},
},
},
type: {
title: "类型",
type: "dict-select",
dict: dict({
data: [
{ label: "收益入账", value: "income", color: "success" },
{ label: "余额抵扣", value: "consume", color: "default" },
{ label: "提现扣减", value: "withdraw", color: "warning" },
],
}),
search: { show: true, col: { span: 3 } },
column: { width: 120 },
},
amount: {
title: "变动金额",
type: "number",
column: {
width: 120,
cellRender({ value }) {
const amount = Number(value || 0);
return <span class={amount < 0 ? "text-green-500" : "text-red-500"}>{moneyText(amount)}</span>;
},
},
},
balanceAfter: {
title: "变动后余额",
type: "number",
column: {
width: 130,
cellRender({ value }) {
return <span class="text-red-500">{moneyText(Number(value || 0))}</span>;
},
},
},
remark: {
title: "备注",
type: "text",
column: { minWidth: 220 },
},
},
},
};
}
@@ -0,0 +1,45 @@
<template>
<fs-page>
<template #header>
<div class="title">
{{ pageTitle }}
<span class="sub">{{ pageSub }}</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useRoute } from "vue-router";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
defineOptions({
name: "SysWalletLog",
});
const route = useRoute();
const isUserLog = computed(() => route.query.userId != null);
const pageTitle = computed(() => {
if (isUserLog.value) {
return "用户余额变更记录";
}
return "余额变更记录";
});
const pageSub = computed(() => {
if (isUserLog.value) {
return "查看该用户的余额变更明细";
}
return "查看所有用户的余额变更明细";
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
useMounted(async () => {
await crudExpose.doRefresh();
});
</script>
+1
View File
@@ -1 +1,2 @@
certd_plugin_loadmode=metadata
# certd_release_mode=dev
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Bug Fixes
* [BUG]使用天翼云CDN部署证书时无法匹配域名 ([bb1f232](https://github.com/certd/certd/commit/bb1f232f415c522851dede979f5d609dda844fd3))
* 支持google persist校验方式 ([97d2b50](https://github.com/certd/certd/commit/97d2b5007409ec79ea864d437c473a2f9b5dfae4))
* **config:** 修复环境变量加载逻辑并更新配置文件 ([07f0d3b](https://github.com/certd/certd/commit/07f0d3b4d626628a0e3dfc7e5c52e8c4a98bad47))
* **ctyun-cdn:** 修复天翼云CDN部署插件的域名选择问题 ([ea7d14c](https://github.com/certd/certd/commit/ea7d14c289b8720781bf101bfef59eac7bb03765))
* **webhook:** 修复模版转义换行符,避免手动上传的证书报错 ([2735feb](https://github.com/certd/certd/commit/2735feb3302f8308708dafc8b295adcd4b91d7f0))
### Features
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
### Performance Improvements
* 宝塔站点列表支持搜索和排序 ([4420472](https://github.com/certd/certd/commit/4420472f8fac272d30fb8ccb1cc2ee9763989001))
* 网宿插件双鉴权支持 ([241b2e5](https://github.com/certd/certd/commit/241b2e55848471e61b5d2be0c87a7dc9a0d29557))
* **monitor:** 站点证书监控支持手动选中触发检查 ([f60e63f](https://github.com/certd/certd/commit/f60e63fbaf00460d5050cd7705bd75d57dec202d))
* **open-api:** openkey增加用途备注 ([bbeaf61](https://github.com/certd/certd/commit/bbeaf617d19750515f6eca51863e74e23bc1de50))
* **tencent clb:** 修改所有CLB地域的endpoint为就近接入 ([c427735](https://github.com/certd/certd/commit/c4277350bcc8cf034cf4e76db2a6daa1842bc5d7))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Performance Improvements
* 新增余额管理,管理员充值功能 ([bded368](https://github.com/certd/certd/commit/bded368cbb8974b6937ae260e448c25fae5c33ad))
* **acme-account-access:** AcmeAccount授权允许修改eab,重新生成account ([6a649aa](https://github.com/certd/certd/commit/6a649aaca083ccd3b517f7d676a58220aa33b37d))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
### Bug Fixes
* **plugin-service:** 修复同步自己开发的云端插件不显示已安装状态问题 ([dac94b6](https://github.com/certd/certd/commit/dac94b64f0ceb7176e2cf07ce220be0aa5ed98d2))
* **plugin:** 修复插件配置保存与加载的相关问题 ([bd94334](https://github.com/certd/certd/commit/bd9433478a43d5d64ce6370386d0fe0aaff07d2e))
### Performance Improvements
* **plugin:** add Fastly custom TLS certificates integration ([#797](https://github.com/certd/certd/issues/797)) [@szhu25](https://github.com/szhu25) ([2a2e963](https://github.com/certd/certd/commit/2a2e963914c4dfc192cbe0a4bbb9c115e626da01))
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
### Bug Fixes
@@ -0,0 +1,24 @@
-- 客户端登记表:certd-client 心跳上报后记录在线状态;客户端自身信息保留独立列,站点统计等快照存 content JSON,后续扩展直接往 JSON 加字段。
CREATE TABLE `cd_client`
(
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
`user_id` bigint NOT NULL,
`project_id` bigint,
`client_id` varchar(64) NOT NULL,
`key_id` varchar(64),
`machine_name` varchar(255),
`version` varchar(64),
`os` varchar(64),
`content` longtext,
`last_heartbeat_at` bigint NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE = InnoDB ROW_FORMAT = DYNAMIC;
CREATE INDEX `index_client_user_id` ON `cd_client` (`user_id`);
CREATE INDEX `index_client_project_id` ON `cd_client` (`project_id`);
CREATE UNIQUE INDEX `index_client_user_client` ON `cd_client` (`user_id`, `client_id`);
-- 开放接口密钥用途备注
ALTER TABLE cd_open_key ADD COLUMN `remark` varchar(512);
@@ -0,0 +1,24 @@
-- 客户端登记表:certd-client 心跳上报后记录在线状态;客户端自身信息保留独立列,站点统计等快照存 content JSON,后续扩展直接往 JSON 加字段。
CREATE TABLE "cd_client"
(
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
"user_id" bigint NOT NULL,
"project_id" bigint,
"client_id" varchar(64) NOT NULL,
"key_id" varchar(64),
"machine_name" varchar(255),
"version" varchar(64),
"os" varchar(64),
"content" text,
"last_heartbeat_at" bigint NOT NULL,
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_client_user_id" ON "cd_client" ("user_id");
CREATE INDEX "index_client_project_id" ON "cd_client" ("project_id");
CREATE UNIQUE INDEX "index_client_user_client" ON "cd_client" ("user_id", "client_id");
-- 开放接口密钥用途备注
ALTER TABLE cd_open_key ADD COLUMN "remark" varchar(512);
@@ -0,0 +1,24 @@
-- 客户端登记表:certd-client 心跳上报后记录在线状态;客户端自身信息保留独立列,站点统计等快照存 content JSON,后续扩展直接往 JSON 加字段。
CREATE TABLE "cd_client"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer NOT NULL,
"project_id" integer,
"client_id" varchar(64) NOT NULL,
"key_id" varchar(64),
"machine_name" varchar(255),
"version" varchar(64),
"os" varchar(64),
"content" text,
"last_heartbeat_at" integer NOT NULL,
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_client_user_id" ON "cd_client" ("user_id");
CREATE INDEX "index_client_project_id" ON "cd_client" ("project_id");
CREATE UNIQUE INDEX "index_client_user_client" ON "cd_client" ("user_id", "client_id");
-- 开放接口密钥用途备注
ALTER TABLE cd_open_key ADD COLUMN "remark" varchar(512);
+13 -3
View File
@@ -76,15 +76,20 @@ async function genMetadata() {
fs.rmSync("./metadata", { recursive: true });
fs.mkdirSync("./metadata", { recursive: true });
const warnings = []
for (const key in modules) {
const module = modules[key];
const entry = Object.entries(module);
if (entry.length > 1) {
console.log(`[warning] 文件 ${key} 导出了 ${entry.length} 个对象: ${entry.map(([name, value]) => name).join(", ")}`);
}
// if (entry.length > 1) {
// warnings.push(`[warning] 文件 ${key} 导出了 ${entry.length} 个对象: ${entry.map(([name, value]) => name).join(", ")}`);
// }
const isManyExport = entry.length > 1;
for (const [name, value] of entry) {
//如果有define属性
if (value.define) {
if (isManyExport) {
throw new Error(`插件文件禁止导出多个对象, ${key} 导出了 ${entry.length} 个对象: ${entry.map(([name, value]) => name).join(", ")}`);
}
//那么就是插件
let location = key.substring(4);
location = location.substring(0, location.length - 3);
@@ -121,6 +126,11 @@ async function genMetadata() {
}
}
}
if (warnings.length > 0) {
for (const warning of warnings) {
console.log(warning);
}
}
}
async function genPluginMd() {
@@ -43,7 +43,7 @@ input:
return {
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
disabled: ctx.compute(({form})=> !!form.access?.account && !form.access?.editAccount)
}
}
@@ -85,7 +85,7 @@ input:
return form.access?._needEAB === true;
}),
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
disabled: ctx.compute(({form})=> !!form.access?.account && !form.access?.editAccount)
}
}
@@ -103,7 +103,7 @@ input:
return form.access?._needEAB === true;
}),
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
disabled: ctx.compute(({form})=> !!form.access?.account && !form.access?.editAccount)
}
}
@@ -9,7 +9,9 @@ input:
placeholder: api token,用户 API 令牌
helper: |-
前往 [获取API令牌](https://dash.cloudflare.com/profile/api-tokens),注意是令牌,不是密钥。
token权限必须包含:[Zone区域-Zone区域-Edit编辑], [Zone区域-DNS-Edit编辑]
token权限必须包含两条:
[Zone区域-Zone区域-Edit编辑]
[Zone区域-DNS-Edit编辑]
required: true
encrypt: true
proxy:
@@ -0,0 +1,28 @@
name: fastly
title: Fastly授权
icon: simple-icons:fastly
desc: Fastly CDN / TLS Custom Certificates API 授权
input:
apiKey:
title: API Token
component:
placeholder: Fastly API Key / Token
helper: 前往 Fastly Account Settings -> Personal Access Tokens 创建 Token
required: true
encrypt: true
proxy:
title: HTTP代理
component:
placeholder: http://xxxx.xxx.xx:10811
helper: 可选:是否使用 HTTP 代理访问 Fastly API
required: false
encrypt: false
testRequest:
title: 测试
component:
name: api-test
action: TestRequest
helper: 测试授权是否正确
pluginType: access
type: builtIn
scriptFilePath: /plugins/plugin-fastly/access.js
@@ -3,25 +3,87 @@ title: 网宿授权
desc: ''
icon: svg:icon-wangsu
input:
authType:
title: 鉴权方式
value: aksk
component:
name: a-select
vModel: value
allowClear: false
options:
- label: AccessKey签名
value: aksk
- label: ApiKey(CDN Pro必须用这个)
value: apikey
helper: 网宿接口鉴权方式
required: false
accessKeyId:
title: accessKeyId
component:
name: a-input
vModel: value
placeholder: accessKeyId
component:
name: a-input
vModel: value
helper: '[点击前往获取AccessKey](https://console.wangsu.com/account/accessKey?rsr=ws)'
encrypt: false
required: true
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return !form.access.authType || form.access.authType === 'aksk';
})
}
accessKeySecret:
title: accessKeySecret
component:
name: a-input
vModel: value
placeholder: accessKeySecret
component:
name: a-input
vModel: value
encrypt: true
required: true
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return !form.access.authType || form.access.authType === 'aksk';
})
}
apiUser:
title: API账号名
component:
name: a-input
vModel: value
placeholder: API账号名
helper: 控制台里的 API 账号名,与 API Key 配套使用
encrypt: false
required: false
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return form.access.authType === 'apikey';
})
}
apiKey:
title: API Key
component:
name: a-input
vModel: value
placeholder: API Key
helper: 控制台里的 API Key,[点击前往获取](https://console.wangsu.com/account/apiManage)
encrypt: true
required: false
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return form.access.authType === 'apikey';
})
}
testRequest:
title: 测试
component:

Some files were not shown because too many files have changed in this diff Show More